diff --git a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties index 96d76fe34a..559e9bba86 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties @@ -24,6 +24,17 @@ VisualizationPanel.jButton6.text=Hierarchy VisualizationPanel.jButton7.text=Circle VisualizationPanel.jButton8.text=Organic VisualizationPanel.fitGraphButton.text= +VisualizationPanel.jTextArea1.text=Right-click an account in the Browse Accounts table, and select 'Visualize' to begin. +VisualizationPanel.jLabel1.text=Layouts: +VisualizationPanel.zoomLabel.text=100% +VisualizationPanel.jLabel2.text=Zoom: +VisualizationPanel.fitZoomButton.toolTipText=fit visualization +VisualizationPanel.fitZoomButton.text= +VisualizationPanel.zoomActualButton.toolTipText=reset zoom +VisualizationPanel.zoomActualButton.text= +VisualizationPanel.zoomInButton.toolTipText=Zoom in +VisualizationPanel.zoomInButton.text= +VisualizationPanel.zoomOutButton.toolTipText=Zoom out VisualizationPanel.zoomOutButton.text= # To change this license header, choose License Headers in Project Properties. # To change this template file, choose Tools | Templates @@ -32,14 +43,4 @@ VisualizationPanel.circleLayoutButton.text=Circle VisualizationPanel.organicLayoutButton.text=Organic VisualizationPanel.fastOrganicLayoutButton.text=Fast Organic VisualizationPanel.hierarchyLayoutButton.text=Hierarchy -VisualizationPanel.zoomLabel.text=100% -VisualizationPanel.jLabel1.text=Layouts: -VisualizationPanel.jLabel2.text=Zoom: -VisualizationPanel.fitZoomButton.toolTipText=fit visualization -VisualizationPanel.fitZoomButton.text= -VisualizationPanel.jTextArea1.text=Right-click an account in the Browse Accounts table, and select 'Visualize' to begin. -VisualizationPanel.zoomActualButton.toolTipText=reset zoom -VisualizationPanel.zoomActualButton.text= -VisualizationPanel.zoomInButton.toolTipText=Zoom in -VisualizationPanel.zoomInButton.text= -VisualizationPanel.zoomOutButton.toolTipText=Zoom out +VisualizationPanel.clearVizButton.text_1=Clear Viz. diff --git a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java index 94d96c4456..68309f5195 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java @@ -50,9 +50,49 @@ 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(); + + private final PropertyChangeListener focusPropertyListener = new PropertyChangeListener() { + /** + * 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. + * + * @param focusEvent The focus change event. + */ + @Override + public void propertyChange(final PropertyChangeEvent focusEvent) { + if (focusEvent.getPropertyName().equalsIgnoreCase("focusOwner")) { + final Component newFocusOwner = (Component) focusEvent.getNewValue(); + + if (newFocusOwner == null) { + return; + } + 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 @@ -78,11 +118,13 @@ public final class MessageBrowser extends JPanel implements ExplorerManager.Prov Bundle.MessageBrowser_DataResultViewerTable_title())); messagesResultPanel.open(); - //add listener that maintains correct selection in the Global Actions Context - KeyboardFocusManager.getCurrentKeyboardFocusManager() - .addPropertyChangeListener("focusOwner", focusPropertyListener); - this.tableEM.addPropertyChangeListener(new PropertyChangeListener() { + /** + * Listener that pushes selections in the tableEM (the Accounts + * table) into the Messages table. + * + * @param pce The ExplorerManager event. + */ @Override public void propertyChange(PropertyChangeEvent pce) { if (pce.getPropertyName().equals(ExplorerManager.PROP_SELECTED_NODES)) { @@ -130,6 +172,14 @@ public final class MessageBrowser extends JPanel implements ExplorerManager.Prov return proxyLookup; } + @Override + public void addNotify() { + super.addNotify(); + //add listener that maintains correct selection in the Global Actions Context + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .addPropertyChangeListener("focusOwner", focusPropertyListener); + } + @Override public void removeNotify() { super.removeNotify(); @@ -178,38 +228,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/communications/ModifiableProxyLookup.java b/Core/src/org/sleuthkit/autopsy/communications/ModifiableProxyLookup.java index cc8657d638..ad3babc504 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/ModifiableProxyLookup.java +++ b/Core/src/org/sleuthkit/autopsy/communications/ModifiableProxyLookup.java @@ -37,8 +37,8 @@ final class ModifiableProxyLookup extends ProxyLookup { * * @param lookups The new Lookups to delegate to. */ - void setNewLookups(final Lookup... lookups) { - /* default */ + public void setNewLookups(final Lookup... lookups) { + setLookups(lookups); } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form index d73c5e907c..9d5722443e 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form @@ -11,7 +11,7 @@ - + @@ -68,15 +68,15 @@ - - - + + + @@ -92,7 +92,11 @@ + + + + @@ -103,7 +107,11 @@ - + + + + + @@ -112,11 +120,7 @@ - - - - - + @@ -137,6 +141,8 @@ + + @@ -204,6 +210,9 @@ + + + @@ -213,11 +222,11 @@ + + - - @@ -232,11 +241,11 @@ + + - - @@ -251,11 +260,11 @@ + + - - @@ -270,11 +279,11 @@ + + - - @@ -295,6 +304,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java index ca6973573b..d6047768dd 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java @@ -39,6 +39,7 @@ import com.mxgraph.util.mxUndoableEdit; import com.mxgraph.view.mxGraph; import java.awt.BorderLayout; import java.awt.Color; +import java.awt.Cursor; import java.awt.Dimension; import java.awt.Frame; import java.awt.event.ActionEvent; @@ -80,6 +81,7 @@ import org.openide.util.NbBundle; import org.openide.util.lookup.ProxyLookup; import org.sleuthkit.autopsy.casemodule.Case; import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.progress.ModalDialogProgressIndicator; @@ -105,16 +107,16 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(VisualizationPanel.class.getName()); private static final String BASE_IMAGE_PATH = "/org/sleuthkit/autopsy/communications/images"; - static final private ImageIcon pinIcon = - new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--pin.png")); - static final private ImageIcon addPinIcon = - new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--plus.png")); - static final private ImageIcon unpinIcon = - new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--minus.png")); - static final private ImageIcon unlockIcon = - new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_unlocked.png")); - static final private ImageIcon lockIcon = - new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_locked.png")); + static final private ImageIcon pinIcon + = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--pin.png")); + static final private ImageIcon addPinIcon + = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--plus.png")); + static final private ImageIcon unpinIcon + = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--minus.png")); + static final private ImageIcon unlockIcon + = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_unlocked.png")); + static final private ImageIcon lockIcon + = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_locked.png")); private static final String CANCEL = Bundle.VisualizationPanel_cancelButton_text(); @@ -167,13 +169,17 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider //install rubber band selection handler rubberband = new mxRubberband(graphComponent); - final mxEventSource.mxIEventListener scaleListener = (Object sender, mxEventObject evt) -> - zoomLabel.setText(DecimalFormat.getPercentInstance().format(graph.getView().getScale())); + final mxEventSource.mxIEventListener scaleListener = (Object sender, mxEventObject evt) + -> zoomLabel.setText(DecimalFormat.getPercentInstance().format(graph.getView().getScale())); graph.getView().addListener(mxEvent.SCALE, scaleListener); graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, scaleListener); - //right click handler graphComponent.getGraphControl().addMouseWheelListener(new MouseAdapter() { + /** + * Translate mouse wheel events into zooming. + * + * @param event The MouseWheelEvent + */ @Override public void mouseWheelMoved(final MouseWheelEvent event) { super.mouseWheelMoved(event); @@ -186,6 +192,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider }); graphComponent.getGraphControl().addMouseListener(new MouseAdapter() { + /** + * Right click handler: show context menu. + * + * @param event The MouseEvent + */ @Override public void mouseClicked(final MouseEvent event) { super.mouseClicked(event); @@ -246,8 +257,8 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider //feed selection to explorermanager graph.getSelectionModel().addListener(null, new SelectionListener()); - final mxEventSource.mxIEventListener undoListener = (Object sender, mxEventObject evt) -> - undoManager.undoableEditHappened((mxUndoableEdit) evt.getProperty("edit")); + final mxEventSource.mxIEventListener undoListener = (Object sender, mxEventObject evt) + -> undoManager.undoableEditHappened((mxUndoableEdit) evt.getProperty("edit")); graph.getModel().addListener(mxEvent.UNDO, undoListener); graph.getView().addListener(mxEvent.UNDO, undoListener); @@ -339,12 +350,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider windowAncestor = (Frame) SwingUtilities.getAncestorOfClass(Frame.class, this); try { - commsManager = Case.getCurrentCase().getSleuthkitCase().getCommunicationsManager(); - } catch (IllegalStateException ex) { - logger.log(Level.SEVERE, "Can't get CommunicationsManager when there is no case open.", ex); + commsManager = Case.getOpenCase().getSleuthkitCase().getCommunicationsManager(); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error getting CommunicationsManager for the current case.", ex); - + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Can't get CommunicationsManager when there is no case open.", ex); } Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> { @@ -399,6 +409,8 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider fitZoomButton = new JButton(); jLabel2 = new JLabel(); zoomLabel = new JLabel(); + clearVizButton = new JButton(); + jSeparator2 = new JToolBar.Separator(); setLayout(new BorderLayout()); @@ -407,11 +419,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider borderLayoutPanel.setLayout(new BorderLayout()); - jTextArea1.setBackground(new Color(240, 240, 240)); jTextArea1.setColumns(20); jTextArea1.setLineWrap(true); jTextArea1.setRows(5); jTextArea1.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.jTextArea1.text")); // NOI18N + jTextArea1.setBackground(new Color(240, 240, 240)); GroupLayout placeHolderPanelLayout = new GroupLayout(placeHolderPanel); placeHolderPanel.setLayout(placeHolderPanelLayout); @@ -472,11 +484,13 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } }); + jSeparator1.setOrientation(SwingConstants.VERTICAL); + zoomOutButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-out-red.png"))); // NOI18N zoomOutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.text")); // NOI18N - zoomOutButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.toolTipText")); // NOI18N zoomOutButton.setFocusable(false); zoomOutButton.setHorizontalTextPosition(SwingConstants.CENTER); + zoomOutButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.toolTipText")); // NOI18N zoomOutButton.setVerticalTextPosition(SwingConstants.BOTTOM); zoomOutButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -486,9 +500,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider zoomInButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-in-green.png"))); // NOI18N zoomInButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.text")); // NOI18N - zoomInButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.toolTipText")); // NOI18N zoomInButton.setFocusable(false); zoomInButton.setHorizontalTextPosition(SwingConstants.CENTER); + zoomInButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.toolTipText")); // NOI18N zoomInButton.setVerticalTextPosition(SwingConstants.BOTTOM); zoomInButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -498,9 +512,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider zoomActualButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-actual.png"))); // NOI18N zoomActualButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.text")); // NOI18N - zoomActualButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.toolTipText")); // NOI18N zoomActualButton.setFocusable(false); zoomActualButton.setHorizontalTextPosition(SwingConstants.CENTER); + zoomActualButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.toolTipText")); // NOI18N zoomActualButton.setVerticalTextPosition(SwingConstants.BOTTOM); zoomActualButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -510,9 +524,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider fitZoomButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-fit.png"))); // NOI18N fitZoomButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.text")); // NOI18N - fitZoomButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.toolTipText")); // NOI18N fitZoomButton.setFocusable(false); fitZoomButton.setHorizontalTextPosition(SwingConstants.CENTER); + fitZoomButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.toolTipText")); // NOI18N fitZoomButton.setVerticalTextPosition(SwingConstants.BOTTOM); fitZoomButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -524,11 +538,25 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider zoomLabel.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomLabel.text")); // NOI18N + clearVizButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/broom.png"))); // NOI18N + clearVizButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.clearVizButton.text_1")); // NOI18N + clearVizButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + clearVizButtonActionPerformed(evt); + } + }); + + jSeparator2.setOrientation(SwingConstants.VERTICAL); + GroupLayout toolbarLayout = new GroupLayout(toolbar); toolbar.setLayout(toolbarLayout); toolbarLayout.setHorizontalGroup(toolbarLayout.createParallelGroup(GroupLayout.LEADING) .add(toolbarLayout.createSequentialGroup() + .addContainerGap() + .add(clearVizButton) .add(3, 3, 3) + .add(jSeparator1, GroupLayout.PREFERRED_SIZE, 10, GroupLayout.PREFERRED_SIZE) + .add(5, 5, 5) .add(jLabel1) .addPreferredGap(LayoutStyle.RELATED) .add(fastOrganicLayoutButton) @@ -539,7 +567,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider .addPreferredGap(LayoutStyle.RELATED) .add(circleLayoutButton) .addPreferredGap(LayoutStyle.RELATED) - .add(jSeparator1, GroupLayout.PREFERRED_SIZE, 10, GroupLayout.PREFERRED_SIZE) + .add(jSeparator2, GroupLayout.PREFERRED_SIZE, 10, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.RELATED) + .add(jLabel2) + .addPreferredGap(LayoutStyle.RELATED) + .add(zoomLabel) .addPreferredGap(LayoutStyle.RELATED) .add(zoomOutButton, GroupLayout.PREFERRED_SIZE, 32, GroupLayout.PREFERRED_SIZE) .addPreferredGap(LayoutStyle.RELATED) @@ -548,11 +580,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider .add(zoomActualButton, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE) .addPreferredGap(LayoutStyle.RELATED) .add(fitZoomButton, GroupLayout.PREFERRED_SIZE, 32, GroupLayout.PREFERRED_SIZE) - .addPreferredGap(LayoutStyle.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .add(jLabel2) - .addPreferredGap(LayoutStyle.RELATED) - .add(zoomLabel) - .add(27, 27, 27)) + .addContainerGap(12, Short.MAX_VALUE)) ); toolbarLayout.setVerticalGroup(toolbarLayout.createParallelGroup(GroupLayout.LEADING) .add(toolbarLayout.createSequentialGroup() @@ -569,7 +597,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider .add(zoomActualButton, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .add(fitZoomButton, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .add(jLabel2) - .add(zoomLabel)) + .add(zoomLabel) + .add(clearVizButton) + .add(jSeparator2, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .add(3, 3, 3)) ); @@ -612,6 +642,18 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider morph(hierarchicalLayout); }//GEN-LAST:event_hierarchyLayoutButtonActionPerformed + private void clearVizButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_clearVizButtonActionPerformed + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + graph.getModel().beginUpdate(); + pinnedAccountModel.clear(); + graph.clear(); + rebuildGraph(); + // Updates the display + graph.getModel().endUpdate(); + setCursor(Cursor.getDefaultCursor()); + + }//GEN-LAST:event_clearVizButtonActionPerformed + private void applyOrganicLayout(int iterations) { organicLayout.setMaxIterations(iterations); morph(organicLayout); @@ -652,7 +694,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider ModalDialogProgressIndicator progress = new ModalDialogProgressIndicator(windowAncestor, "Computing layout", new String[]{CANCEL}, CANCEL, cancelationListener); SwingWorker morphWorker = new SwingWorker() { @Override - protected Void doInBackground() throws Exception { + protected Void doInBackground() { progress.start("Computing layout"); layout.execute(graph.getDefaultParent()); if (isCancelled()) { @@ -694,12 +736,14 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider // Variables declaration - do not modify//GEN-BEGIN:variables private JPanel borderLayoutPanel; private JButton circleLayoutButton; + private JButton clearVizButton; private JButton fastOrganicLayoutButton; private JButton fitZoomButton; private JButton hierarchyLayoutButton; private JLabel jLabel1; private JLabel jLabel2; private JToolBar.Separator jSeparator1; + private JToolBar.Separator jSeparator2; private JTextArea jTextArea1; private JButton organicLayoutButton; private JPanel placeHolderPanel; @@ -856,7 +900,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public void actionPerformed(ActionEvent e) { + public void actionPerformed(ActionEvent event) { progress.setCancelling("Cancelling..."); cancellable.cancel(true); progress.finish(); diff --git a/Core/src/org/sleuthkit/autopsy/communications/images/broom.png b/Core/src/org/sleuthkit/autopsy/communications/images/broom.png new file mode 100644 index 0000000000..2c6152ef62 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/communications/images/broom.png differ 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..6cd1559825 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ModifiableProxyLookup.java @@ -0,0 +1,43 @@ +/* + * 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. + */ + public void setNewLookups(final Lookup... lookups) { + setLookups(lookups); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java index 2b0bf5d383..3d19bd4226 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java @@ -18,6 +18,11 @@ */ package org.sleuthkit.autopsy.timeline; +import java.awt.BorderLayout; +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; @@ -36,12 +41,14 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javax.swing.JComponent; +import javax.swing.JPanel; 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,6 +59,7 @@ 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.corecomponentinterfaces.DataContent; import org.sleuthkit.autopsy.corecomponents.DataContentPanel; import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.corecomponents.TableFilterNode; @@ -84,22 +92,72 @@ 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; - /** - * Listener that drives the result viewer or content viewer (depending on - * view mode) according to the controller's selected event IDs - */ + /** Lookup that will be exposed through the (Global Actions Context) */ + private final ModifiableProxyLookup proxyLookup = new ModifiableProxyLookup(); + + private final PropertyChangeListener focusPropertyListener = new PropertyChangeListener() { + /** + * 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. + * + * @param focusEvent The focus change event. + */ + @Override + public void propertyChange(final PropertyChangeEvent focusEvent) { + if (focusEvent.getPropertyName().equalsIgnoreCase("focusOwner")) { + final Component newFocusOwner = (Component) focusEvent.getNewValue(); + + if (newFocusOwner == null) { + return; + } + 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())); + + } + } + } + }; + @NbBundle.Messages({"TimelineTopComponent.selectedEventListener.errorMsg=There was a problem getting the content for the selected event."}) private final InvalidationListener selectedEventsListener = new InvalidationListener() { + /** + * Listener that drives the result viewer or content viewer (depending + * on view mode) according to the controller's selected event IDs + * + * @param observable Observable that was invalidated. Usually + * irrelevant. + */ @Override public void invalidated(Observable observable) { List selectedEventIDs = controller.getSelectedEventIDs(); @@ -118,10 +176,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 @@ -172,7 +230,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer */ SwingUtilities.invokeLater(() -> { splitYPane.remove(contentViewerPanel); - if ((horizontalSplitPane.getParent() == splitYPane) == false) { + if (horizontalSplitPane.getParent() != splitYPane) { splitYPane.setBottomComponent(horizontalSplitPane); horizontalSplitPane.setRightComponent(contentViewerPanel); } @@ -197,7 +255,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 +264,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 +272,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 +283,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); } /** @@ -255,7 +318,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer final TabPane leftTabPane = new TabPane(filterTab, eventsTreeTab); VBox.setVgrow(leftTabPane, Priority.ALWAYS); controller.viewModeProperty().addListener(viewMode -> { - if (controller.getViewMode().equals(ViewMode.DETAIL) == false) { + if (controller.getViewMode() != ViewMode.DETAIL) { //if view mode is not details, switch back to the filter tab leftTabPane.getSelectionModel().select(filterTab); } @@ -383,11 +446,22 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer public void componentOpened() { super.componentOpened(); WindowManager.getDefault().setTopComponentFloating(this, true); + + //add listener that maintains correct selection in the Global Actions Context + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .addPropertyChangeListener("focusOwner", focusPropertyListener); + } + + @Override + protected void componentClosed() { + super.componentClosed(); + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .removePropertyChangeListener("focusOwner", focusPropertyListener); } @Override public ExplorerManager getExplorerManager() { - return em; + return explorerManager; } /** @@ -413,6 +487,52 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer .withZone(TimeLineController.getJodaTimeZone()) .toString(zonedFormatter); return Bundle.TimeLineResultView_startDateToEndDate_text(start, end); + + } + } + + /** + * 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 private static class DataContentExplorerPanel extends JPanel implements ExplorerManager.Provider, DataContent { + + private final ExplorerManager explorerManager = new ExplorerManager(); + private final DataContentPanel wrapped; + + private 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. + */ + private void initialize() { + add(wrapped, BorderLayout.CENTER); } } }