diff --git a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java index 94d96c4456..1258c2d525 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java @@ -50,9 +50,46 @@ public final class MessageBrowser extends JPanel implements ExplorerManager.Prov private final DataResultPanel messagesResultPanel; /* lookup that will be exposed through the (Global Actions Context) */ private final ModifiableProxyLookup proxyLookup = new ModifiableProxyLookup(); - /* Listener that keeps the proxyLookup in sync with the focused area of the - * UI. */ - private final FocusPropertyListener focusPropertyListener = new FocusPropertyListener(); + + /** + * Listener that keeps the proxyLookup in sync with the focused area of the + * UI. + * + * Since the embedded MessageContentViewer (attachments panel) is not in its + * own TopComponenet, its selection does not get proxied into the Global + * Actions Context (GAC), and many of the available actions don't work on + * it. Further, we can't put the selection from both the Messages table and + * the Attachments table in the GAC because they could both include + * AbstractFiles, muddling the selection seen by the actions. Instead, + * depending on where the focus is in the window, we want to put different + * Content in the Global Actions Context to be picked up by, e.g., the + * tagging actions. The best way I could figure to do this was to listen to + * all focus events and swap out what is in the lookup appropriately. An + * alternative to this would be to investigate using the ContextAwareAction + * interface. + * + * @see org.sleuthkit.autopsy.timeline.TimeLineTopComponent for a similar + * situation and a similar solution. + */ + private final PropertyChangeListener focusPropertyListener = new PropertyChangeListener() { + + @Override + public void propertyChange(final PropertyChangeEvent focusEvent) { + if (focusEvent.getPropertyName().equalsIgnoreCase("focusOwner")) { + final Component newFocusOwner = (Component) focusEvent.getNewValue(); + + if (newFocusOwner != null) { + if (isDescendingFrom(newFocusOwner, messageDataContent)) { + //if the focus owner is within the MessageContentViewer ( the attachments table) + proxyLookup.setNewLookups(createLookup(messageDataContent.getExplorerManager(), getActionMap())); + } else if (isDescendingFrom(newFocusOwner, messagesResultPanel)) { + //... or if it is within the Messages table. + proxyLookup.setNewLookups(createLookup(gacExplorerManager, getActionMap())); + } + } + } + } + }; /** * Constructs the right hand side of the Communications Visualization Tool @@ -178,38 +215,4 @@ public final class MessageBrowser extends JPanel implements ExplorerManager.Prov private javax.swing.JSplitPane splitPane; // End of variables declaration//GEN-END:variables - /** - * Since the embedded MessageContentViewer (attachments panel) is not in its - * own TopComponenet, its selection does not get proxied into the Global - * Actions Context (GAC), and many of the available actions don't work on - * it. Further, we can't put the selection from both the Messages table and - * the Attachments table in the GAC because they could include have - * AbstractFiles, muddling the selection seen by the actions. Instead, - * depending on where the focus is in the window, we want to put different - * Content in the Global Actions Context to be picked up by, e.g., the - * tagging actions. The best way I could figure to do this was to listen to - * all focus events and swap out what is in the lookup appropriately. An - * alternative to this would be to investigate using the ContextAwareAction - * interface. - */ - private class FocusPropertyListener implements PropertyChangeListener { - - @Override - public void propertyChange(final PropertyChangeEvent focusEvent) { - - if (focusEvent.getPropertyName().equalsIgnoreCase("focusOwner")) { - final Component newFocusOwner = (Component) focusEvent.getNewValue(); - - if (newFocusOwner != null) { - if (isDescendingFrom(newFocusOwner, messageDataContent)) { - //if the focus owner is within the MessageContentViewer ( the attachments table) - proxyLookup.setNewLookups(createLookup(messageDataContent.getExplorerManager(), getActionMap())); - } else if (isDescendingFrom(newFocusOwner, messagesResultPanel)) { - //... or if it is within the Messages table. - proxyLookup.setNewLookups(createLookup(gacExplorerManager, getActionMap())); - } - } - } - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/MessageDataContent.java b/Core/src/org/sleuthkit/autopsy/communications/MessageDataContent.java index ad7229157d..33df9bd3d9 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/MessageDataContent.java +++ b/Core/src/org/sleuthkit/autopsy/communications/MessageDataContent.java @@ -25,7 +25,12 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataContent; /** * Extends MessageContentViewer so that it implements DataContent and can be set - * as the only ContentViewer for a DataResultPanel + * as the only ContentViewer for a DataResultPanel. In addition it provides an + * ExplorerManager. + * + * @see org.sleuthkit.autopsy.timeline.DataContentExplorerPanel for another + * solution to a very similar problem. + * */ final class MessageDataContent extends MessageContentViewer implements DataContent, ExplorerManager.Provider { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/DataContentExplorerPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/DataContentExplorerPanel.java new file mode 100644 index 0000000000..d87cae18c0 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/DataContentExplorerPanel.java @@ -0,0 +1,72 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +import java.awt.BorderLayout; +import java.beans.PropertyChangeEvent; +import javax.swing.JPanel; +import org.openide.explorer.ExplorerManager; +import org.openide.nodes.Node; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataContent; +import org.sleuthkit.autopsy.corecomponents.DataContentPanel; + +/** + * Panel that wraps a DataContentPanel and implements ExplorerManager.Provider. + * This allows the explorer manager found by the DataContentPanel to be + * controlled easily. + * + * @see org.sleuthkit.autopsy.communications.MessageDataContent for another + * solution to a very similar problem. + */ +final class DataContentExplorerPanel extends JPanel implements ExplorerManager.Provider, DataContent { + + private final ExplorerManager explorerManager = new ExplorerManager(); + private final DataContentPanel wrapped; + + DataContentExplorerPanel() { + super(new BorderLayout()); + wrapped = DataContentPanel.createInstance(); + } + + @Override + public ExplorerManager getExplorerManager() { + return explorerManager; + } + + @Override + public void setNode(Node selectedNode) { + wrapped.setNode(selectedNode); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + wrapped.propertyChange(evt); + } + + /** + * Initialize the contents of this panel for use. Specifically add the + * wrapped DataContentPanel to the AWT/Swing containment hierarchy. This + * will trigger the addNotify() method of the embeded Message + * MessageContentViewer causing it to look for a ExplorerManager; it should + * find the one provided by this DataContentExplorerPanel. + */ + void initialize() { + add(wrapped, BorderLayout.CENTER); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ModifiableProxyLookup.java b/Core/src/org/sleuthkit/autopsy/timeline/ModifiableProxyLookup.java new file mode 100644 index 0000000000..bbeea62e1a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ModifiableProxyLookup.java @@ -0,0 +1,46 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +import org.openide.util.Lookup; +import org.openide.util.lookup.ProxyLookup; + +/** + * Extension of ProxyLookup that exposes the ability to change the Lookups + * delegated to. + * + */ +final class ModifiableProxyLookup extends ProxyLookup { + + ModifiableProxyLookup(final Lookup... lookups) { + super(lookups); + } + + /** + * Set the Lookups delegated to by this lookup. + * + * @param lookups The new Lookups to delegate to. + */ + void setNewLookups(final Lookup... lookups) { + /* + * default + */ + setLookups(lookups); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java index 2b0bf5d383..920e3c8fa5 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java @@ -18,6 +18,10 @@ */ package org.sleuthkit.autopsy.timeline; +import java.awt.Component; +import java.awt.KeyboardFocusManager; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.beans.PropertyVetoException; import java.util.List; import java.util.logging.Level; @@ -37,11 +41,12 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javax.swing.JComponent; import javax.swing.SwingUtilities; +import static javax.swing.SwingUtilities.isDescendingFrom; import org.controlsfx.control.Notifications; import org.joda.time.Interval; import org.joda.time.format.DateTimeFormatter; import org.openide.explorer.ExplorerManager; -import org.openide.explorer.ExplorerUtils; +import static org.openide.explorer.ExplorerUtils.createLookup; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Node; @@ -52,7 +57,6 @@ import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.actions.AddBookmarkTagAction; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.corecomponents.DataContentPanel; import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.corecomponents.TableFilterNode; import org.sleuthkit.autopsy.coreutils.Logger; @@ -84,16 +88,60 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer private static final Logger LOGGER = Logger.getLogger(TimeLineTopComponent.class.getName()); @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - private final DataContentPanel contentViewerPanel; + private final DataContentExplorerPanel contentViewerPanel; @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - private DataResultPanel dataResultPanel; + private final DataResultPanel dataResultPanel; @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - private final ExplorerManager em = new ExplorerManager(); + private final ExplorerManager explorerManager = new ExplorerManager(); private final TimeLineController controller; + /** Lookup that will be exposed through the (Global Actions Context) */ + private final ModifiableProxyLookup proxyLookup = new ModifiableProxyLookup(); + + /** + * Listener that keeps the proxyLookup in sync with the focused area of the + * UI. + * + * Since the embedded MessageContentViewer (attachments panel) inside the + * DataContentPanel is not in its own TopComponenet, its selection does not + * get proxied into the Global Actions Context (GAC) automatically, and many + * of the available actions don't work on it. Further, we can't put the + * selection from both the Result table and the Attachments table in the GAC + * because they could bouth include AbstractFiles, muddling the selection + * seen by the actions. Instead, depending on where the focus is in the + * window, we want to put different Content in the Global Actions Context to + * be picked up by, e.g., the tagging actions. The best way I could figure + * to do this was to listen to all focus events and swap out what is in the + * lookup appropriately. An alternative to this would be to investigate + * using the ContextAwareAction interface. + * + * @see org.sleuthkit.autopsy.communications.MessageBrowser for a similar + * situation and a similar solution. + */ + private final PropertyChangeListener focusPropertyListener = new PropertyChangeListener() { + + @Override + public void propertyChange(final PropertyChangeEvent focusEvent) { + + if (focusEvent.getPropertyName().equalsIgnoreCase("focusOwner")) { + final Component newFocusOwner = (Component) focusEvent.getNewValue(); + + if (newFocusOwner != null) { + if (isDescendingFrom(newFocusOwner, contentViewerPanel)) { + //if the focus owner is within the MessageContentViewer (the attachments table) + proxyLookup.setNewLookups(createLookup(contentViewerPanel.getExplorerManager(), getActionMap())); + } else if (isDescendingFrom(newFocusOwner, TimeLineTopComponent.this)) { + //... or if it is within the Results table. + proxyLookup.setNewLookups(createLookup(explorerManager, getActionMap())); + } + } + } + } + }; + /** * Listener that drives the result viewer or content viewer (depending on * view mode) according to the controller's selected event IDs @@ -118,10 +166,10 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer SwingUtilities.invokeLater(() -> { //set generic container node as root context - em.setRootContext(new AbstractNode(children)); + explorerManager.setRootContext(new AbstractNode(children)); try { //set selected nodes for actions - em.setSelectedNodes(childArray); + explorerManager.setSelectedNodes(childArray); } catch (PropertyVetoException ex) { //I don't know why this would ever happen. LOGGER.log(Level.SEVERE, "Selecting the event node was vetoed.", ex); // NON-NLS @@ -197,7 +245,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer */ public TimeLineTopComponent(TimeLineController controller) { initComponents(); - associateLookup(ExplorerUtils.createLookup(em, getActionMap())); + associateLookup(proxyLookup); setName(NbBundle.getMessage(TimeLineTopComponent.class, "CTL_TimeLineTopComponent")); getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(AddBookmarkTagAction.BOOKMARK_SHORTCUT, "addBookmarkTag"); //NON-NLS @@ -206,7 +254,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer this.controller = controller; //create linked result and content views - contentViewerPanel = DataContentPanel.createInstance(); + contentViewerPanel = new DataContentExplorerPanel(); dataResultPanel = DataResultPanel.createInstanceUninitialized("", "", Node.EMPTY, 0, contentViewerPanel); //add them to bottom splitpane @@ -214,6 +262,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer horizontalSplitPane.setRightComponent(contentViewerPanel); dataResultPanel.open(); //get the explorermanager + contentViewerPanel.initialize(); Platform.runLater(this::initFXComponents); @@ -224,6 +273,10 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer //Listen to ViewMode and adjust GUI componenets as needed. controller.viewModeProperty().addListener(viewMode -> syncViewMode()); syncViewMode(); + + //add listener that maintains correct selection in the Global Actions Context + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .addPropertyChangeListener("focusOwner", focusPropertyListener); } /** @@ -387,7 +440,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer @Override public ExplorerManager getExplorerManager() { - return em; + return explorerManager; } /**