diff --git a/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java b/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java new file mode 100644 index 0000000000..ef751d262c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java @@ -0,0 +1,65 @@ +/* + * 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.communications; + +import java.util.Collection; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.JMenuItem; +import org.openide.util.Utilities; +import org.openide.util.actions.Presenter; + +/** + * Base class for actions that act on the selected AccountDeviceInstanceKeys. + * getPopupPresenter() provides a JMenuItem that works (i.e., has an icon) in + * custom context menus and also in the Netbeans Explorer views. + */ +abstract class AbstractCVTAction extends AbstractAction implements Presenter.Popup { + + /** + * Get the selected accounts that will be acted upon. + * + * @return The selected accounts + */ + Collection getSelectedAccounts() { + return Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class); + } + + @Override + public JMenuItem getPopupPresenter() { + JMenuItem presenter = new JMenuItem(this); + presenter.setText(getActionDisplayName()); + presenter.setIcon(getIcon()); + return presenter; + } + + /** + * The name/text of the action as displayed in a menu. + * + * @return The diaplay name of this action + */ + abstract String getActionDisplayName(); + + /** + * The icon to use for this icon. + * + * @return An ImageIcon used to represent this action. + */ + abstract ImageIcon getIcon(); +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java index 40cb15d27d..044cec322f 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java @@ -18,17 +18,13 @@ */ package org.sleuthkit.autopsy.communications; -import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import javax.swing.AbstractAction; import javax.swing.Action; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; -import org.openide.util.Utilities; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.datamodel.Account; @@ -106,54 +102,4 @@ final class AccountDeviceInstanceNode extends AbstractNode { actions.add(ResetAndPinAccountsAction.getInstance()); return actions.toArray(new Action[actions.size()]); } - - /** - * Action that pins the selected AccountDeviceInstances to the - * visualization. - */ - static private class PinAccountsAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - private final static PinAccountsAction instance = new PinAccountsAction(); - - private static PinAccountsAction getInstance() { - return instance; - } - - private PinAccountsAction() { - super("Add Account(s) to Visualization"); - } - - @Override - public void actionPerformed(ActionEvent e) { - Collection lookupAll = - Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class); - CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(lookupAll, false)); - } - } - - /** - * Action that pins the selected AccountDeviceInstances to the - * visualization. - */ - static private class ResetAndPinAccountsAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - private final static ResetAndPinAccountsAction instance = new ResetAndPinAccountsAction(); - - private static ResetAndPinAccountsAction getInstance() { - return instance; - } - - private ResetAndPinAccountsAction() { - super("Visualize Account(s)"); - } - - @Override - public void actionPerformed(ActionEvent e) { - Collection lookupAll = - Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class); - CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(lookupAll, true)); - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties index 559e9bba86..358971eaa8 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties @@ -20,9 +20,6 @@ CVTTopComponent.vizPanel.TabConstraints.tabTitle=Visualize CVTTopComponent.accountsBrowser.TabConstraints.tabTitle_1=Browse CVTTopComponent.browseVisualizeTabPane.AccessibleContext.accessibleName=Visualize CVTTopComponent.vizPanel.TabConstraints.tabTitle_1=Visualize -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: @@ -36,11 +33,8 @@ 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 -# and open the template in the editor. VisualizationPanel.circleLayoutButton.text=Circle VisualizationPanel.organicLayoutButton.text=Organic VisualizationPanel.fastOrganicLayoutButton.text=Fast Organic -VisualizationPanel.hierarchyLayoutButton.text=Hierarchy +VisualizationPanel.hierarchyLayoutButton.text=Hierarchical VisualizationPanel.clearVizButton.text_1=Clear Viz. diff --git a/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java b/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java index e0f945bb69..6a6a60d3da 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java +++ b/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java @@ -21,7 +21,6 @@ package org.sleuthkit.autopsy.communications; import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; import java.util.Collection; -import java.util.Set; import org.sleuthkit.datamodel.CommunicationsFilter; /** @@ -79,7 +78,7 @@ final class CVTEvents { return accountDeviceInstances; } - public UnpinAccountsEvent(Set accountDeviceInstances) { + UnpinAccountsEvent(Collection accountDeviceInstances) { this.accountDeviceInstances = ImmutableSet.copyOf(accountDeviceInstances); } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java b/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java index ddeb89ada6..716d00656c 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java +++ b/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java @@ -25,7 +25,6 @@ import com.google.common.collect.MultimapBuilder; import com.mxgraph.model.mxCell; import com.mxgraph.model.mxICell; import com.mxgraph.util.mxConstants; -import com.mxgraph.view.mxCellState; import com.mxgraph.view.mxGraph; import com.mxgraph.view.mxStylesheet; import java.io.InputStream; @@ -84,17 +83,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 +114,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); } /** @@ -255,20 +241,20 @@ final class CommunicationsGraph extends mxGraph { */ private class RebuildWorker extends SwingWorker { - private final ProgressIndicator progress; + private final ProgressIndicator progressIndicator; private final CommunicationsManager commsManager; private final CommunicationsFilter currentFilter; RebuildWorker(ProgressIndicator progress, CommunicationsManager commsManager, CommunicationsFilter currentFilter) { - this.progress = progress; + this.progressIndicator = progress; this.currentFilter = currentFilter; this.commsManager = commsManager; } @Override - protected Void doInBackground() throws Exception { - progress.start("Loading accounts"); + protected Void doInBackground() { + progressIndicator.start("Loading accounts"); int progressCounter = 0; try { /** @@ -278,7 +264,7 @@ final class CommunicationsGraph extends mxGraph { for (final AccountDeviceInstanceKey adiKey : pinnedAccountModel.getPinnedAccounts()) { if (isCancelled()) { break; - } + } //get accounts related to pinned account final List relatedAccountDeviceInstances = commsManager.getRelatedAccountDeviceInstances(adiKey.getAccountDeviceInstance(), currentFilter); @@ -290,7 +276,7 @@ final class CommunicationsGraph extends mxGraph { final AccountDeviceInstanceKey relatedADIKey = new AccountDeviceInstanceKey(relatedADI, currentFilter, adiRelationshipsCount); relatedAccounts.put(relatedADI, relatedADIKey); //store related accounts } - progress.progress(++progressCounter); + progressIndicator.progress(++progressCounter); } Set accounts = relatedAccounts.keySet(); @@ -298,9 +284,9 @@ final class CommunicationsGraph extends mxGraph { Map relationshipCounts = commsManager.getRelationshipCountsPairwise(accounts, currentFilter); int total = relationshipCounts.size(); - int k = 0; - String progressText = ""; - progress.switchToDeterminate("", 0, total); + int progress = 0; + String progressText = ""; + progressIndicator.switchToDeterminate("", 0, total); for (Map.Entry entry : relationshipCounts.entrySet()) { Long count = entry.getValue(); AccountPair relationshipKey = entry.getKey(); @@ -308,15 +294,14 @@ 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(); } - progress.progress(progressText, k++); + progressIndicator.progress(progressText, progress++); } } catch (TskCoreException tskCoreException) { logger.log(Level.SEVERE, "Error", tskCoreException); - } finally { } return null; @@ -332,7 +317,7 @@ final class CommunicationsGraph extends mxGraph { } catch (CancellationException ex) { logger.log(Level.INFO, "Graph visualization cancelled"); } finally { - progress.finish(); + progressIndicator.finish(); } } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/EventHandler.java b/Core/src/org/sleuthkit/autopsy/communications/EventHandler.java deleted file mode 100644 index f1acedf1cf..0000000000 --- a/Core/src/org/sleuthkit/autopsy/communications/EventHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.communications; - -/** - * - */ -public interface EventHandler { - - void handle(T event); -} diff --git a/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java b/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java index f26e9515b3..7d8b8c083d 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java @@ -18,20 +18,19 @@ */ 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.Collection; import java.util.HashSet; import java.util.Set; -class LockedVertexModel { - - void registerhandler(EventHandler handler) { - eventBus.register(handler); - } - - void unregisterhandler(EventHandler handler) { - eventBus.unregister(handler); - } +/** + * Model of which vertices in a graph are locked ( not moveable by layout + * algorithms). + * + */ +final class LockedVertexModel { private final EventBus eventBus = new EventBus(); @@ -42,30 +41,34 @@ class LockedVertexModel { */ private final Set lockedVertices = new HashSet<>(); - LockedVertexModel() { + void registerhandler(Object handler) { + eventBus.register(handler); + } + + void unregisterhandler(Object handler) { + eventBus.unregister(handler); } /** - * 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) { @@ -77,21 +80,36 @@ class LockedVertexModel { lockedVertices.clear(); } - static class VertexLockEvent { + boolean isEmpty() { + return lockedVertices.isEmpty(); + } - private final mxCell vertex; + /** + * Event that represents a change in the locked state of one or more + * vertices. + */ + final static class VertexLockEvent { - public mxCell getVertex() { - return vertex; + private final boolean locked; + private final Set vertices; + + /** + * @return The vertices whose locked state has changed. + */ + public Set getVertices() { + return vertices; } - public boolean isVertexLocked() { + /** + * @return True if the vertices are locked, False if the vertices are + * unlocked. + */ + public boolean isLocked() { return locked; } - 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/MessageBrowser.java b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java index 68309f5195..7b90357993 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java @@ -35,7 +35,6 @@ import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.corecomponents.TableFilterNode; import org.sleuthkit.autopsy.directorytree.DataResultFilterNode; -import org.sleuthkit.datamodel.AccountDeviceInstance; /** * The right hand side of the CVT. Has a DataResultPanel to show a listing of @@ -151,10 +150,10 @@ public final class MessageBrowser extends JPanel implements ExplorerManager.Prov //Use lookup here? final AccountDeviceInstanceNode adiNode = (AccountDeviceInstanceNode) selectedNodes[0]; - final Set accountDeviceInstances = new HashSet<>(); + final Set accountDeviceInstances = new HashSet<>(); for (final Node n : selectedNodes) { //Use lookup here? - accountDeviceInstances.add(((AccountDeviceInstanceNode) n).getAccountDeviceInstance()); + accountDeviceInstances.add(((AccountDeviceInstanceNode) n).getAccountDeviceInstanceKey()); } return SelectionNode.createFromAccounts(accountDeviceInstances, adiNode.getFilter(), adiNode.getCommsManager()); } diff --git a/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java new file mode 100644 index 0000000000..52136d1b1a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java @@ -0,0 +1,59 @@ +/* + * 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.communications; + +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; + +/** + * Action that pins the AccountDevicesIntanceKeys in the ActionsGlobalContext to + * the visualizaion + */ +@NbBundle.Messages({"PinAccountsAction.pluralText=Add Selected Accounts to Visualization", + "PinAccountsAction.singularText=Add Selected Account to Visualization"}) +final class PinAccountsAction extends AbstractCVTAction { + + static private final ImageIcon ICON = ImageUtilities.loadImageIcon( + "/org/sleuthkit/autopsy/communications/images/marker--plus.png", false); + private static final String SINGULAR_TEXT = Bundle.PinAccountsAction_singularText(); + private static final String PLURAL_TEXT = Bundle.PinAccountsAction_pluralText(); + + private static final PinAccountsAction instance = new PinAccountsAction(); + + static PinAccountsAction getInstance() { + return instance; + } + + @Override + public void actionPerformed(ActionEvent event) { + CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(getSelectedAccounts(), false)); + } + + @Override + protected String getActionDisplayName() { + return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT; + } + + @Override + ImageIcon getIcon() { + return ICON; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java b/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java index d5f7a7cc03..f7fb7d1232 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java @@ -19,22 +19,31 @@ package org.sleuthkit.autopsy.communications; import com.google.common.collect.ImmutableSet; +import com.google.common.eventbus.EventBus; import java.util.HashSet; import java.util.Set; +/** + * Model of what accounts are pinned to a visualization. + */ 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; + private final EventBus eventBus = new EventBus(); + + void registerhandler(Object handler) { + eventBus.register(handler); + } + + void unregisterhandler(Object handler) { + eventBus.unregister(handler); } boolean isAccountPinned(AccountDeviceInstanceKey account) { diff --git a/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java new file mode 100644 index 0000000000..385ac3348b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java @@ -0,0 +1,59 @@ +/* + * 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.communications; + +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; + +/** + * Action that clears any pinned accounts and pins the AcountDevicesInstanceKeys + * in the ActionsGlobalContext to the visualization. + */ +@NbBundle.Messages(value = {"ResetAndPinAccountsAction.singularText=Visualize Only Selected Account", + "ResetAndPinAccountsAction.pluralText=Visualize Only Selected Accounts"}) +final class ResetAndPinAccountsAction extends AbstractCVTAction { + + private static final ImageIcon ICON = ImageUtilities.loadImageIcon( + "/org/sleuthkit/autopsy/communications/images/marker--pin.png", false); + private static final String SINGULAR_TEXT = Bundle.ResetAndPinAccountsAction_singularText(); + private static final String PLURAL_TEXT = Bundle.ResetAndPinAccountsAction_pluralText(); + + private static final ResetAndPinAccountsAction instance = new ResetAndPinAccountsAction(); + + static ResetAndPinAccountsAction getInstance() { + return instance; + } + + @Override + public void actionPerformed(ActionEvent event) { + CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(getSelectedAccounts(), true)); + } + + @Override + protected String getActionDisplayName() { + return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT; + } + + @Override + ImageIcon getIcon() { + return ICON; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java b/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java index 40739b49cc..1618bb9ecc 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java @@ -24,10 +24,13 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.logging.Level; +import java.util.stream.Collectors; import org.openide.nodes.AbstractNode; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; import org.openide.nodes.Node; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AccountDeviceInstance; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -44,23 +47,27 @@ import org.sleuthkit.datamodel.TskCoreException; */ final class SelectionNode extends AbstractNode { - private SelectionNode(Children children) { - super(children); + private SelectionNode(Children children, Lookup lookup) { + super(children, lookup); } static SelectionNode createFromAccountsAndRelationships( Set edgeRelationshipArtifacts, - Set accountDeviceInstances, + Set accountDeviceInstanceKeys, CommunicationsFilter filter, CommunicationsManager commsManager) { + Set accountDeviceInstances = accountDeviceInstanceKeys.stream() + .map(AccountDeviceInstanceKey::getAccountDeviceInstance) + .collect(Collectors.toSet()); + SelectionNode node = new SelectionNode(Children.create( new RelationshipChildren( edgeRelationshipArtifacts, accountDeviceInstances, commsManager, filter), - true)); + true), Lookups.fixed(accountDeviceInstanceKeys.toArray())); //This is not good for internationalization!!! String name = ""; @@ -82,7 +89,7 @@ final class SelectionNode extends AbstractNode { } static SelectionNode createFromAccounts( - Set accountDeviceInstances, + Set accountDeviceInstances, CommunicationsFilter filter, CommunicationsManager commsManager) { @@ -122,9 +129,9 @@ final class SelectionNode extends AbstractNode { } @Override - protected Node createNodeForKey(Content t) { - if (t instanceof BlackboardArtifact) { - return new RelationshipNode((BlackboardArtifact) t); + protected Node createNodeForKey(Content content) { + if (content instanceof BlackboardArtifact) { + return new RelationshipNode((BlackboardArtifact) content); } else { throw new UnsupportedOperationException("Cannot create a RelationshipNode for non BlackboardArtifact content."); } diff --git a/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java new file mode 100644 index 0000000000..ba0bbc545b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java @@ -0,0 +1,59 @@ +/* + * 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.communications; + +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; + +/** + * Action that unpins the AcccountDeviceInstanceKeys in the ActionsGlobalContext + * form the visualization. + */ +@NbBundle.Messages({"UnpinAccountsAction.pluralText=Remove Selected Accounts", + "UnpinAccountsAction.singularText=Remove Selected Account"}) +final class UnpinAccountsAction extends AbstractCVTAction { + + static final private ImageIcon ICON = ImageUtilities.loadImageIcon( + "/org/sleuthkit/autopsy/communications/images/marker--minus.png", false); + private static final String SINGULAR_TEXT = Bundle.UnpinAccountsAction_singularText(); + private static final String PLURAL_TEXT = Bundle.UnpinAccountsAction_pluralText(); + + private static final UnpinAccountsAction instance = new UnpinAccountsAction(); + + static UnpinAccountsAction getInstance() { + return instance; + } + + @Override + public void actionPerformed(final ActionEvent event) { + CVTEvents.getCVTEventBus().post(new CVTEvents.UnpinAccountsEvent(getSelectedAccounts())); + } + + @Override + String getActionDisplayName() { + return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT; + } + + @Override + ImageIcon getIcon() { + return ICON; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java index 965832b3bd..4f6b53ae39 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java @@ -22,14 +22,12 @@ import com.google.common.eventbus.Subscribe; import com.mxgraph.layout.hierarchical.mxHierarchicalLayout; import com.mxgraph.layout.mxCircleLayout; import com.mxgraph.layout.mxFastOrganicLayout; -import com.mxgraph.layout.mxGraphLayout; import com.mxgraph.layout.mxIGraphLayout; import com.mxgraph.layout.mxOrganicLayout; import com.mxgraph.model.mxCell; import com.mxgraph.model.mxICell; import com.mxgraph.swing.handler.mxRubberband; import com.mxgraph.swing.mxGraphComponent; -import com.mxgraph.swing.util.mxMorphing; import com.mxgraph.util.mxEvent; import com.mxgraph.util.mxEventObject; import com.mxgraph.util.mxEventSource; @@ -37,7 +35,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; @@ -53,17 +53,18 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyVetoException; import java.text.DecimalFormat; import java.util.Arrays; -import static java.util.Collections.singleton; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.Map; import java.util.concurrent.Future; import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; 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; @@ -86,12 +87,11 @@ import org.openide.util.Lookup; 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.MessageNotifyUtil; 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; @@ -107,23 +107,17 @@ 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; 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")); + @NbBundle.Messages("VisualizationPanel.cancelButton.text=Cancel") private static final String CANCEL = Bundle.VisualizationPanel_cancelButton_text(); private final ExplorerManager vizEM = new ExplorerManager(); @@ -138,27 +132,20 @@ 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 mxFastOrganicLayout fastOrganicLayout; - private final mxCircleLayout circleLayout; - private final mxOrganicLayout organicLayout; - private final mxHierarchicalLayout hierarchyLayout; + private final mxRubberband rubberband; //NOPMD We keep a referenec as insurance to prevent garbage collection @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(); - private final Map layoutButtons = new HashMap<>(); - private mxGraphLayout currentLayout; + private final Map layoutButtons = new HashMap<>(); + private NamedGraphLayout currentLayout; public VisualizationPanel() { initComponents(); - graph = new CommunicationsGraph(); - pinnedAccountModel = graph.getPinnedAccountModel(); - lockedVertexModel = graph.getLockedVertexModel(); - + graph = new CommunicationsGraph(pinnedAccountModel, lockedVertexModel); graphComponent = new mxGraphComponent(graph); graphComponent.setAutoExtend(true); @@ -172,41 +159,43 @@ 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(this); + 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); final GraphMouseListener graphMouseListener = new GraphMouseListener(); - graphComponent.getGraphControl().addMouseWheelListener(graphMouseListener); graphComponent.getGraphControl().addMouseListener(graphMouseListener); final MessageBrowser messageBrowser = new MessageBrowser(vizEM, gacEM); splitPane.setRightComponent(messageBrowser); proxyLookup = new ProxyLookup( - messageBrowser.getLookup(), - ExplorerUtils.createLookup(vizEM, getActionMap())); + ExplorerUtils.createLookup(vizEM, getActionMap()), + messageBrowser.getLookup() + ); //feed selection to explorermanager - graph.getSelectionModel().addListener(null, new SelectionListener()); + graph.getSelectionModel().addListener(mxEvent.CHANGE, new SelectionListener()); 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); - - fastOrganicLayout = new mxFastOrganicLayoutImpl(graph); - circleLayout = new mxCircleLayoutImpl(graph); - organicLayout = new mxOrganicLayoutImpl(graph); + + FastOrganicLayoutImpl fastOrganicLayout = new FastOrganicLayoutImpl(graph); + CircleLayoutImpl circleLayout = new CircleLayoutImpl(graph); + OrganicLayoutImpl organicLayout = new OrganicLayoutImpl(graph); organicLayout.setMaxIterations(10); - hierarchyLayout = new mxHierarchicalLayoutImpl(graph); + HierarchicalLayoutImpl hierarchyLayout = new HierarchicalLayoutImpl(graph); //local method to configure layout buttons - BiConsumer configure = (layoutButton, layout) -> { + BiConsumer configure = (layoutButton, layout) -> { layoutButtons.put(layout, layoutButton); layoutButton.addActionListener(event -> applyLayout(layout)); }; @@ -216,7 +205,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider configure.accept(fastOrganicLayoutButton, fastOrganicLayout); configure.accept(hierarchyLayoutButton, hierarchyLayout); - applyLayout(circleLayout); + applyLayout(fastOrganicLayout); } /** @@ -225,24 +214,35 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider * @param layout the value of layout */ @Override - public Lookup getLookup() { return proxyLookup; } @Subscribe - void handleUnPinEvent(final CVTEvents.UnpinAccountsEvent pinEvent) { + void handle(LockedVertexModel.VertexLockEvent event) { + final Set vertices = event.getVertices(); + mxGraphView view = graph.getView(); + vertices.forEach(vertex -> { + final mxCellState state = view.getState(vertex, true); + view.updateLabel(state); + view.updateLabelBounds(state); + view.updateBoundingBox(state); + graphComponent.redraw(state); + }); + } + + @Subscribe + void handle(final CVTEvents.UnpinAccountsEvent pinEvent) { graph.getModel().beginUpdate(); pinnedAccountModel.unpinAccount(pinEvent.getAccountDeviceInstances()); graph.clear(); rebuildGraph(); // Updates the display graph.getModel().endUpdate(); - } @Subscribe - void handlePinEvent(final CVTEvents.PinAccountsEvent pinEvent) { + void handle(final CVTEvents.PinAccountsEvent pinEvent) { graph.getModel().beginUpdate(); if (pinEvent.isReplace()) { graph.resetGraph(); @@ -251,19 +251,16 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider rebuildGraph(); // Updates the display graph.getModel().endUpdate(); - } @Subscribe - void handleFilterEvent(final CVTEvents.FilterChangeEvent filterChangeEvent) { - + void handle(final CVTEvents.FilterChangeEvent filterChangeEvent) { graph.getModel().beginUpdate(); graph.clear(); currentFilter = filterChangeEvent.getNewFilter(); rebuildGraph(); // Updates the display graph.getModel().endUpdate(); - } @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @@ -288,15 +285,12 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider if (worker.isCancelled()) { graph.resetGraph(); rebuildGraph(); - morph(organicLayout); - } else { - morph(fastOrganicLayout); } + applyLayout(currentLayout); } }); worker.execute(); - } } @@ -313,7 +307,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider logger.log(Level.SEVERE, "Can't get CommunicationsManager when there is no case open.", ex); } - Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> { + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), evt -> { graph.getModel().beginUpdate(); try { graph.resetGraph(); @@ -330,15 +324,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider logger.log(Level.SEVERE, "Error getting CommunicationsManager for the current case.", ex); } } - }); } - @Override - public void removeNotify() { - super.removeNotify(); - } - /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always @@ -563,17 +551,54 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider }//GEN-LAST:event_zoomOutButtonActionPerformed /** + * Apply the given layout. The given layout becomes the current layout. The + * layout is computed in the background. * - * @param layoutButton the value of layoutButton - * @param layout the value of layout + * @param layout The layout to apply. */ - private void applyLayout(mxGraphLayout layout) { + @NbBundle.Messages({"VisualizationPanel.computingLayout=Computing Layout", + "# {0} - layout name", + "VisualizationPanel.layoutFailWithLockedVertices.text={0} layout failed with locked vertices. Unlock some vertices or try a different layout.", + "# {0} - layout name", + "VisualizationPanel.layoutFail.text={0} layout failed. Try a different layout."}) + private void applyLayout(NamedGraphLayout layout) { currentLayout = layout; layoutButtons.forEach((layoutKey, button) - -> button.setFont(button.getFont().deriveFont(layoutKey == layout ? Font.BOLD : Font.PLAIN)) ); - morph(layout); - } + -> button.setFont(button.getFont().deriveFont(layoutKey == layout ? Font.BOLD : Font.PLAIN))); + ModalDialogProgressIndicator progressIndicator = new ModalDialogProgressIndicator(windowAncestor, Bundle.VisualizationPanel_computingLayout()); + progressIndicator.start(Bundle.VisualizationPanel_computingLayout()); + + new SwingWorker() { + @Override + protected Void doInBackground() { + graph.getModel().beginUpdate(); + try { + layout.execute(graph.getDefaultParent()); + fitGraph(); + } finally { + graph.getModel().endUpdate(); + progressIndicator.finish(); + } + return null; + } + + @Override + protected void done() { + try { + get(); + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.WARNING, "CVT graph layout failed.", ex); + if (lockedVertexModel.isEmpty()) { + MessageNotifyUtil.Message.error(Bundle.VisualizationPanel_layoutFail_text(layout.getDisplayName())); + } else { + MessageNotifyUtil.Message.error(Bundle.VisualizationPanel_layoutFailWithLockedVertices_text(layout.getDisplayName())); + } + undoManager.undo(); + } + } + }.execute(); + } private void clearVizButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_clearVizButtonActionPerformed setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); @@ -584,7 +609,6 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider // Updates the display graph.getModel().endUpdate(); setCursor(Cursor.getDefaultCursor()); - }//GEN-LAST:event_clearVizButtonActionPerformed private void fitGraph() { @@ -609,57 +633,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider final Dimension size = graphComponent.getSize(); final double widthFactor = size.getWidth() / boundsForCells.getWidth(); + final double heightFactor = size.getHeight() / boundsForCells.getHeight(); - graphComponent.zoom(widthFactor); - + graphComponent.zoom((heightFactor + widthFactor) / 2.0); } - 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); - SwingWorker morphWorker = new SwingWorker() { - @Override - protected Void doInBackground() { - progress.start("Computing layout"); - layout.execute(graph.getDefaultParent()); - if (isCancelled()) { - progress.finish(); - return null; - } - mxMorphing morph = new mxMorphing(graphComponent, 20, 1.2, 20) { - @Override - public void updateAnimation() { - fireEvent(new mxEventObject(mxEvent.EXECUTE)); - super.updateAnimation(); //To change body of generated methods, choose Tools | Templates. - } - - }; - morph.addListener(mxEvent.EXECUTE, (Object sender, mxEventObject evt) -> { - if (isCancelled()) { - morph.stopAnimation(); - } - }); - morph.addListener(mxEvent.DONE, (Object sender, mxEventObject event) -> { - graph.getModel().endUpdate(); - if (isCancelled()) { - undoManager.undo(); - } else { - fitGraph(); - } - progress.finish(); - }); - - morph.startAnimation(); - return null; - - } - }; - cancelationListener.configure(morphWorker, progress); - morphWorker.execute(); - } // Variables declaration - do not modify//GEN-BEGIN:variables private JPanel borderLayoutPanel; @@ -683,6 +661,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") @@ -694,7 +676,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider if (selectionCells.length > 0) { mxICell[] selectedCells = Arrays.asList(selectionCells).toArray(new mxCell[selectionCells.length]); HashSet relationshipSources = new HashSet<>(); - HashSet adis = new HashSet<>(); + HashSet adis = new HashSet<>(); for (mxICell cell : selectedCells) { if (cell.isEdge()) { mxICell source = (mxICell) graph.getModel().getTerminal(cell, true); @@ -711,7 +693,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider logger.log(Level.SEVERE, " Error getting relationsips....", tskCoreException); } } else if (cell.isVertex()) { - adis.add(((AccountDeviceInstanceKey) cell.getValue()).getAccountDeviceInstance()); + adis.add((AccountDeviceInstanceKey) cell.getValue()); } } @@ -727,9 +709,20 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } } - final private class mxFastOrganicLayoutImpl extends mxFastOrganicLayout { + /** + * Extend mxIGraphLayout with a getDisplayName method, + */ + private interface NamedGraphLayout extends mxIGraphLayout { - mxFastOrganicLayoutImpl(mxGraph graph) { + String getDisplayName(); + } + + /** + * Extension of mxFastOrganicLayout that ignores locked vertices. + */ + final private class FastOrganicLayoutImpl extends mxFastOrganicLayout implements NamedGraphLayout { + + FastOrganicLayoutImpl(mxGraph graph) { super(graph); } @@ -740,18 +733,26 @@ 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 are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Fast Organic"; + } } - final private class mxCircleLayoutImpl extends mxCircleLayout { + /** + * Extension of mxCircleLayout that ignores locked vertices. + */ + final private class CircleLayoutImpl extends mxCircleLayout implements NamedGraphLayout { - mxCircleLayoutImpl(mxGraph graph) { + CircleLayoutImpl(mxGraph graph) { super(graph); setResetEdges(true); } @@ -763,18 +764,26 @@ 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 are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Circle"; + } } - final private class mxOrganicLayoutImpl extends mxOrganicLayout { + /** + * Extension of mxOrganicLayout that ignores locked vertices. + */ + final private class OrganicLayoutImpl extends mxOrganicLayout implements NamedGraphLayout { - mxOrganicLayoutImpl(mxGraph graph) { + OrganicLayoutImpl(mxGraph graph) { super(graph); setResetEdges(true); } @@ -786,18 +795,26 @@ 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 are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Organic"; + } } - final private class mxHierarchicalLayoutImpl extends mxHierarchicalLayout { + /** + * Extension of mxHierarchicalLayout that ignores locked vertices. + */ + final private class HierarchicalLayoutImpl extends mxHierarchicalLayout implements NamedGraphLayout { - mxHierarchicalLayoutImpl(mxGraph graph) { + HierarchicalLayoutImpl(mxGraph graph) { super(graph); } @@ -808,15 +825,24 @@ 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 are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Hierarchical"; + } } + /** + * Listener that closses the given ModalDialogProgressIndicator and cancels + * the future. + */ private class CancelationListener implements ActionListener { private Future cancellable; @@ -833,9 +859,12 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider cancellable.cancel(true); progress.finish(); } - } + /** + * Mouse Adapter for the graphComponent. Handles wheel zooming and context + * menus. + */ private class GraphMouseListener extends MouseAdapter { /** @@ -846,9 +875,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider @Override public void mouseWheelMoved(final MouseWheelEvent event) { super.mouseWheelMoved(event); - if (event.getPreciseWheelRotation() > 0) { + if (event.getPreciseWheelRotation() < 0) { graphComponent.zoomIn(); - } else if (event.getPreciseWheelRotation() < 0) { + } else if (event.getPreciseWheelRotation() > 0) { graphComponent.zoomOut(); } } @@ -867,45 +896,71 @@ 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 " + cellAt.getId(), 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 " + cellAt.getId(), lockIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - lockedVertexModel.lockVertex(cellAt); - } - })); + jPopupMenu.add(new JMenuItem(new LockAction(selectedVertices))); } if (pinnedAccountModel.isAccountPinned(adiKey)) { - jPopupMenu.add(new JMenuItem(new AbstractAction("Unpin " + cellAt.getId(), unpinIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - handleUnPinEvent(new CVTEvents.UnpinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()))); - } - })); + jPopupMenu.add(UnpinAccountsAction.getInstance().getPopupPresenter()); } else { - jPopupMenu.add(new JMenuItem(new AbstractAction("Pin " + cellAt.getId(), addPinIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - handlePinEvent(new CVTEvents.PinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()), false)); - } - })); - jPopupMenu.add(new JMenuItem(new AbstractAction("Pin only " + cellAt.getId(), pinIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - handlePinEvent(new CVTEvents.PinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()), true)); - } - })); + jPopupMenu.add(PinAccountsAction.getInstance().getPopupPresenter()); + jPopupMenu.add(ResetAndPinAccountsAction.getInstance().getPopupPresenter()); } jPopupMenu.show(graphComponent.getGraphControl(), event.getX(), event.getY()); } } } } + + /** + * Action that un-locks the selected vertices. + */ + @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); + } + } + + /** + * Action that locks the selected vertices. + */ + @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); + } + } }