diff --git a/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java b/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java index f274ab00d7..98d9aa7d88 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java +++ b/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java @@ -38,6 +38,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; import java.util.logging.Level; import javax.swing.SwingWorker; import org.sleuthkit.autopsy.coreutils.Logger; @@ -84,17 +85,19 @@ final class CommunicationsGraph extends mxGraph { mxStylesheet.getDefaultEdgeStyle().put(mxConstants.STYLE_STARTARROW, mxConstants.NONE); } - /* Map from type specific account identifier to mxCell(vertex). */ + /** Map from type specific account identifier to mxCell(vertex). */ private final Map nodeMap = new HashMap<>(); - /* Map from relationship source (Content) to mxCell (edge). */ + /** Map from relationship source (Content) to mxCell (edge). */ private final Multimap edgeMap = MultimapBuilder.hashKeys().hashSetValues().build(); private final LockedVertexModel lockedVertexModel; private final PinnedAccountModel pinnedAccountModel; - CommunicationsGraph() { + CommunicationsGraph(PinnedAccountModel pinnedAccountModel, LockedVertexModel lockedVertexModel) { super(mxStylesheet); + this.pinnedAccountModel =pinnedAccountModel; + this.lockedVertexModel = lockedVertexModel; //set fixed properties of graph. setAutoSizeCells(true); setCellsCloneable(false); @@ -113,21 +116,6 @@ final class CommunicationsGraph extends mxGraph { setKeepEdgesInBackground(true); setResetEdgesOnMove(true); setHtmlLabels(true); - - lockedVertexModel = new LockedVertexModel(); - lockedVertexModel.registerhandler((LockedVertexModel.VertexLockEvent event) -> { - if (event.isVertexLocked()) { - getView().clear(event.getVertex(), true, true); - getView().validate(); - } else { - final mxCellState state = getView().getState(event.getVertex(), true); - getView().updateLabel(state); - getView().updateLabelBounds(state); - getView().updateBoundingBox(state); - } - }); - - pinnedAccountModel = new PinnedAccountModel(this); } /** @@ -299,7 +287,7 @@ final class CommunicationsGraph extends mxGraph { int total = relationshipCounts.size(); int k = 0; - String progressText = ""; + String progressText = ""; progress.switchToDeterminate("", 0, total); for (Map.Entry entry : relationshipCounts.entrySet()) { Long count = entry.getValue(); @@ -308,7 +296,7 @@ final class CommunicationsGraph extends mxGraph { AccountDeviceInstanceKey account2 = relatedAccounts.get(relationshipKey.getSecond()); if (pinnedAccountModel.isAccountPinned(account1) - || pinnedAccountModel.isAccountPinned(account2)) { + || pinnedAccountModel.isAccountPinned(account2)) { mxCell addEdge = addOrUpdateEdge(count, account1, account2); progressText = addEdge.getId(); } diff --git a/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java b/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java index f26e9515b3..ad189a8bea 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java @@ -18,12 +18,22 @@ */ package org.sleuthkit.autopsy.communications; +import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; import com.mxgraph.model.mxCell; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; -class LockedVertexModel { +/** + * Model of which vertices in a graph are locked ( not moveable by layout + * algorithms). + * + */ +final class LockedVertexModel { void registerhandler(EventHandler handler) { eventBus.register(handler); @@ -42,30 +52,26 @@ class LockedVertexModel { */ private final Set lockedVertices = new HashSet<>(); - LockedVertexModel() { - } - /** - * Lock the given vertex so that applying a layout algorithm doesn't move - * it. The user can still manually position the vertex. + * Lock the given vertices so that applying a layout algorithm doesn't move + * them. The user can still manually position the vertices. * * @param vertex The vertex to lock. */ - void lockVertex(mxCell vertex) { - lockedVertices.add(vertex); - eventBus.post(new VertexLockEvent(vertex, true)); - + void lock(Collection vertices) { + lockedVertices.addAll(vertices); + eventBus.post(new VertexLockEvent(true, vertices)); } /** - * Lock the given vertex so that applying a layout algorithm can move it. + * Unlock the given vertices so that applying a layout algorithm can move + * them. * * @param vertex The vertex to unlock. */ - void unlockVertex(mxCell vertex) { - lockedVertices.remove(vertex); - eventBus.post(new VertexLockEvent(vertex, false)); - + void unlock(Collection vertices) { + lockedVertices.removeAll(vertices); + eventBus.post(new VertexLockEvent(false, vertices)); } boolean isVertexLocked(mxCell vertex) { @@ -79,10 +85,10 @@ class LockedVertexModel { static class VertexLockEvent { - private final mxCell vertex; + private final Set vertices; - public mxCell getVertex() { - return vertex; + public Set getVertices() { + return vertices; } public boolean isVertexLocked() { @@ -90,8 +96,8 @@ class LockedVertexModel { } private final boolean locked; - VertexLockEvent(mxCell vertex, boolean locked) { - this.vertex = vertex; + VertexLockEvent(boolean locked, Collection< mxCell> vertices) { + this.vertices = ImmutableSet.copyOf(vertices); this.locked = locked; } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java b/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java index d5f7a7cc03..beff35173d 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java @@ -25,17 +25,12 @@ import java.util.Set; class PinnedAccountModel { /** - * Set of AccountDeviceInstanceKeys that are 'Pinned' to this graph. Pinned + * Set of AccountDeviceInstanceKeys that are 'Pinned' to the graph. Pinned * accounts are shown regardless of filters, and accounts that are related * to pinned accounts and pass the filters are show. Pinning accounts is the * primary way to populate the graph. */ private final Set pinnedAccountDevices = new HashSet<>(); - private final CommunicationsGraph graph; - - PinnedAccountModel(CommunicationsGraph graph) { - this.graph = graph; - } boolean isAccountPinned(AccountDeviceInstanceKey account) { return pinnedAccountDevices.contains(account); diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java index 559bdf2bd5..d33ca867d7 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java @@ -36,7 +36,9 @@ import com.mxgraph.util.mxPoint; import com.mxgraph.util.mxRectangle; import com.mxgraph.util.mxUndoManager; import com.mxgraph.util.mxUndoableEdit; +import com.mxgraph.view.mxCellState; import com.mxgraph.view.mxGraph; +import com.mxgraph.view.mxGraphView; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; @@ -54,8 +56,11 @@ import java.util.Arrays; import java.util.EnumSet; import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.Future; import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.swing.AbstractAction; import javax.swing.ImageIcon; import javax.swing.JButton; @@ -83,7 +88,6 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.progress.ModalDialogProgressIndicator; -import org.sleuthkit.datamodel.AccountDeviceInstance; import org.sleuthkit.datamodel.CommunicationsFilter; import org.sleuthkit.datamodel.CommunicationsManager; import org.sleuthkit.datamodel.Content; @@ -99,7 +103,6 @@ import org.sleuthkit.datamodel.TskCoreException; * CVTTopComponent when this tab is active allowing for context sensitive * actions to work correctly. */ -@NbBundle.Messages("VisualizationPanel.cancelButton.text=Cancel") final public class VisualizationPanel extends JPanel implements Lookup.Provider { private static final long serialVersionUID = 1L; @@ -110,6 +113,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider static final private ImageIcon lockIcon = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_locked.png")); + @NbBundle.Messages("VisualizationPanel.cancelButton.text=Cancel") private static final String CANCEL = Bundle.VisualizationPanel_cancelButton_text(); private final ExplorerManager vizEM = new ExplorerManager(); @@ -124,7 +128,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider private final CommunicationsGraph graph; private final mxUndoManager undoManager = new mxUndoManager(); - private final mxRubberband rubberband; + private final mxRubberband rubberband; //NOPMD We keep a referenec as insurance to prevent garbage collection private final mxFastOrganicLayout fastOrganicLayout; private final mxCircleLayout circleLayout; @@ -133,14 +137,13 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private SwingWorker worker; - private final PinnedAccountModel pinnedAccountModel; - private final LockedVertexModel lockedVertexModel; + private final PinnedAccountModel pinnedAccountModel = new PinnedAccountModel(); + private final LockedVertexModel lockedVertexModel = new LockedVertexModel(); public VisualizationPanel() { initComponents(); - graph = new CommunicationsGraph(); - pinnedAccountModel = graph.getPinnedAccountModel(); - lockedVertexModel = graph.getLockedVertexModel(); + + graph = new CommunicationsGraph(pinnedAccountModel, lockedVertexModel); fastOrganicLayout = new mxFastOrganicLayoutImpl(graph); circleLayout = new mxCircleLayoutImpl(graph); @@ -159,9 +162,26 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider graphComponent.setBackground(Color.WHITE); borderLayoutPanel.add(graphComponent, BorderLayout.CENTER); - //install rubber band selection handler + //install rubber band other handlers rubberband = new mxRubberband(graphComponent); + lockedVertexModel.registerhandler((LockedVertexModel.VertexLockEvent event) -> { + final Set vertices = event.getVertices(); + mxGraphView view = graph.getView(); + if (event.isVertexLocked()) { + vertices.forEach(vertex -> view.clear(vertex, true, true)); + view.validate(); + } else { + vertices.forEach(vertex -> { + final mxCellState state = view.getState(vertex, true); + view.updateLabel(state); + view.updateLabelBounds(state); + view.updateBoundingBox(state); + }); + } + graphComponent.getGraphControl().repaint(); + }); + final mxEventSource.mxIEventListener scaleListener = (Object sender, mxEventObject evt) -> zoomLabel.setText(DecimalFormat.getPercentInstance().format(graph.getView().getScale())); graph.getView().addListener(mxEvent.SCALE, scaleListener); @@ -199,20 +219,16 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider final JPopupMenu jPopupMenu = new JPopupMenu(); final AccountDeviceInstanceKey adiKey = (AccountDeviceInstanceKey) cellAt.getValue(); + Set selectedVertices + = Stream.of(graph.getSelectionModel().getCells()) + .map(mxCell.class::cast) + .filter(mxCell::isVertex) + .collect(Collectors.toSet()); + if (lockedVertexModel.isVertexLocked(cellAt)) { - jPopupMenu.add(new JMenuItem(new AbstractAction("UnLock", unlockIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - lockedVertexModel.unlockVertex(cellAt); - } - })); + jPopupMenu.add(new JMenuItem(new UnlockAction(selectedVertices))); } else { - jPopupMenu.add(new JMenuItem(new AbstractAction("Lock", lockIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - lockedVertexModel.lockVertex(cellAt); - } - })); + jPopupMenu.add(new JMenuItem(new LockAction(selectedVertices))); } if (pinnedAccountModel.isAccountPinned(adiKey)) { jPopupMenu.add(UnpinAccountsAction.getInstance().getPopupPresenter()); @@ -224,6 +240,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } } } + }); final MessageBrowser messageBrowser = new MessageBrowser(vizEM, gacEM); @@ -650,16 +667,18 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } + @NbBundle.Messages({"Visualization.computingLayout=Computing Layout"}) private void morph(mxIGraphLayout layout) { // layout using morphing graph.getModel().beginUpdate(); CancelationListener cancelationListener = new CancelationListener(); - ModalDialogProgressIndicator progress = new ModalDialogProgressIndicator(windowAncestor, "Computing layout", new String[]{CANCEL}, CANCEL, cancelationListener); + ModalDialogProgressIndicator progress = new ModalDialogProgressIndicator(windowAncestor, + Bundle.Visualization_computingLayout(), new String[]{CANCEL}, CANCEL, cancelationListener); SwingWorker morphWorker = new SwingWorker() { @Override protected Void doInBackground() { - progress.start("Computing layout"); + progress.start(Bundle.Visualization_computingLayout()); layout.execute(graph.getDefaultParent()); if (isCancelled()) { progress.finish(); @@ -719,6 +738,10 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider private JButton zoomOutButton; // End of variables declaration//GEN-END:variables + /** + * Listens to graph selection model and updates ExplorerManager to reflect + * changes in selection. + */ final private class SelectionListener implements mxEventSource.mxIEventListener { @SuppressWarnings("unchecked") @@ -776,7 +799,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x,y variable names are standard if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { @@ -799,7 +822,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x,y variable names are standard if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { @@ -822,7 +845,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x,y variable names are standard if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { @@ -844,7 +867,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x,y variable names are standard if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { @@ -870,4 +893,43 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider progress.finish(); } } + + @NbBundle.Messages({ + "VisualizationPanel.unlockAction.singularText=Unlock Selected Account", + "VisualizationPanel.unlockAction.pluralText=Unlock Selected Accounts",}) + private final class UnlockAction extends AbstractAction { + + private final Set selectedVertices; + + UnlockAction(Set selectedVertices) { + super(selectedVertices.size() > 1 ? Bundle.VisualizationPanel_unlockAction_pluralText() : Bundle.VisualizationPanel_unlockAction_singularText(), + unlockIcon); + this.selectedVertices = selectedVertices; + } + + @Override + + public void actionPerformed(final ActionEvent event) { + lockedVertexModel.unlock(selectedVertices); + } + } + + @NbBundle.Messages({ + "VisualizationPanel.lockAction.singularText=Lock Selected Account", + "VisualizationPanel.lockAction.pluralText=Lock Selected Accounts"}) + private final class LockAction extends AbstractAction { + + private final Set selectedVertices; + + LockAction(Set selectedVertices) { + super(selectedVertices.size() > 1 ? Bundle.VisualizationPanel_lockAction_pluralText() : Bundle.VisualizationPanel_lockAction_singularText(), + lockIcon); + this.selectedVertices = selectedVertices; + } + + @Override + public void actionPerformed(final ActionEvent event) { + lockedVertexModel.lock(selectedVertices); + } + } }