diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java index aca7219998..6ae2e1dcf2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2013-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,10 +18,14 @@ */ package org.sleuthkit.autopsy.timeline; +import com.google.common.collect.Iterables; import java.awt.BorderLayout; import java.util.Collections; import java.util.List; +import java.util.logging.Level; import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; import javafx.embed.swing.JFXPanel; import javafx.scene.Scene; import javafx.scene.control.SplitPane; @@ -34,6 +38,7 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javax.swing.SwingUtilities; +import org.controlsfx.control.Notifications; import org.openide.explorer.ExplorerManager; import org.openide.explorer.ExplorerUtils; import org.openide.util.NbBundle; @@ -46,6 +51,7 @@ import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.actions.Back; import org.sleuthkit.autopsy.timeline.actions.Forward; +import org.sleuthkit.autopsy.timeline.explorernodes.EventNode; import org.sleuthkit.autopsy.timeline.ui.HistoryToolBar; import org.sleuthkit.autopsy.timeline.ui.StatusBar; import org.sleuthkit.autopsy.timeline.ui.TimeLineResultView; @@ -54,9 +60,10 @@ import org.sleuthkit.autopsy.timeline.ui.VisualizationPanel; import org.sleuthkit.autopsy.timeline.ui.detailview.tree.EventsTree; import org.sleuthkit.autopsy.timeline.ui.filtering.FilterSetPanel; import org.sleuthkit.autopsy.timeline.zooming.ZoomSettingsPane; +import org.sleuthkit.datamodel.TskCoreException; /** - * TopComponent for the timeline feature. + * TopComponent for the Timeline feature. */ @TopComponent.Description( preferredID = "TimeLineTopComponent", @@ -67,7 +74,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer private static final Logger LOGGER = Logger.getLogger(TimeLineTopComponent.class.getName()); - private final DataContentPanel dataContentPanel; + private final DataContentPanel contentViewerPanel; private final TimeLineResultView tlResultView; @@ -75,6 +82,36 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer private final TimeLineController controller; + @NbBundle.Messages({ + "TimelineTopComponent.selectedEventListener.errorMsg=There was a problem getting the content for the selected event."}) + /** + * Listener that drives the ContentViewer when in List ViewMode. + */ + private final InvalidationListener selectedEventListener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + if (controller.getSelectedEventIDs().size() == 1) { + try { + EventNode eventNode = EventNode.createEventNode(Iterables.getOnlyElement(controller.getSelectedEventIDs()), controller.getEventsModel()); + SwingUtilities.invokeLater(() -> contentViewerPanel.setNode(eventNode)); + } catch (IllegalStateException ex) { + //Since the case is closed, the user probably doesn't care about this, just log it as a precaution. + LOGGER.log(Level.SEVERE, "There was no case open to lookup the Sleuthkit object backing a SingleEvent.", ex); // NON-NLS + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Failed to lookup Sleuthkit object backing a SingleEvent.", ex); // NON-NLS + Platform.runLater(() -> { + Notifications.create() + .owner(jFXVizPanel.getScene().getWindow()) + .text(Bundle.TimelineTopComponent_selectedEventListener_errorMsg()) + .showError(); + }); + } + } else { + SwingUtilities.invokeLater(() -> contentViewerPanel.setNode(null)); + } + } + }; + public TimeLineTopComponent(TimeLineController controller) { initComponents(); this.controller = controller; @@ -84,9 +121,9 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer setToolTipText(NbBundle.getMessage(TimeLineTopComponent.class, "HINT_TimeLineTopComponent")); setIcon(WindowManager.getDefault().getMainWindow().getIconImage()); //use the same icon as main application - dataContentPanel = DataContentPanel.createInstance(); - this.contentViewerContainerPanel.add(dataContentPanel, BorderLayout.CENTER); - tlResultView = new TimeLineResultView(controller, dataContentPanel); + contentViewerPanel = DataContentPanel.createInstance(); + this.contentViewerContainerPanel.add(contentViewerPanel, BorderLayout.CENTER); + tlResultView = new TimeLineResultView(controller, contentViewerPanel); final DataResultPanel dataResultPanel = tlResultView.getDataResultPanel(); this.resultContainerPanel.add(dataResultPanel, BorderLayout.CENTER); dataResultPanel.open(); @@ -108,6 +145,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer lowerSplitXPane.add(contentViewerContainerPanel); } }); + controller.getSelectedEventIDs().removeListener(selectedEventListener); break; case LIST: /* @@ -119,6 +157,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer splitYPane.add(contentViewerContainerPanel); dataResultPanel.setNode(null); }); + controller.getSelectedEventIDs().addListener(selectedEventListener); break; default: throw new UnsupportedOperationException("Unknown ViewMode: " + controller.getViewMode()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java index abb7245eda..09ffec0ff0 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,21 +31,25 @@ import org.openide.nodes.Children; import org.openide.nodes.PropertySupport; import org.openide.nodes.Sheet; import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.DataModelActionsFactory; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; /** - * * Explorer Node for {@link SingleEvent}s. + * * Explorer Node for SingleEvents. */ -class EventNode extends DisplayableItemNode { +public class EventNode extends DisplayableItemNode { private static final Logger LOGGER = Logger.getLogger(EventNode.class.getName()); @@ -155,10 +159,38 @@ class EventNode extends DisplayableItemNode { } @Override - public void setValue(String t) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + final public void setValue(String t) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { String oldValue = getValue(); value = t; firePropertyChange("time", oldValue, t); // NON-NLS } } + + /** + * Factory method to create an EventNode from the event ID and the events + * model. + * + * @param eventID The ID of the event this node is for. + * @param eventsModel The model that provides access to the events DB. + * + * @return An EventNode with the file (and artifact) backing this event in + * its lookup. + */ + public static EventNode createEventNode(final Long eventID, FilteredEventsModel eventsModel) throws TskCoreException, IllegalStateException { + /* + * Look up the event by id and creata an EventNode with the appropriate + * data in the lookup. + */ + final SingleEvent eventById = eventsModel.getEventById(eventID); + + SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); + AbstractFile file = sleuthkitCase.getAbstractFileById(eventById.getFileID()); + + if (eventById.getArtifactID().isPresent()) { + BlackboardArtifact blackboardArtifact = sleuthkitCase.getBlackboardArtifact(eventById.getArtifactID().get()); + return new EventNode(eventById, file, blackboardArtifact); + } else { + return new EventNode(eventById, file); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java index 6fd3693514..eb84883808 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java @@ -27,15 +27,10 @@ import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; -import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; /** @@ -132,29 +127,19 @@ public class EventRootNode extends DisplayableItemNode { */ return new TooManyNode(eventIDs.size()); } else { - /* - * look up the event by id and creata an EventNode with the - * appropriate data in the lookup. - */ - final SingleEvent eventById = filteredEvents.getEventById(eventID); try { - SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); - AbstractFile file = sleuthkitCase.getAbstractFileById(eventById.getFileID()); - if (file != null) { - if (eventById.getArtifactID().isPresent()) { - BlackboardArtifact blackboardArtifact = sleuthkitCase.getBlackboardArtifact(eventById.getArtifactID().get()); - return new EventNode(eventById, file, blackboardArtifact); - } else { - return new EventNode(eventById, file); - } - } else { - //This should never happen in normal operations - LOGGER.log(Level.WARNING, "Failed to lookup sleuthkit object backing TimeLineEvent."); // NON-NLS - return null; - } - } catch (IllegalStateException | TskCoreException ex) { - //if some how the case was closed or ther is another unspecified exception, just bail out with a warning. - LOGGER.log(Level.WARNING, "Failed to lookup sleuthkit object backing TimeLineEvent.", ex); // NON-NLS + return EventNode.createEventNode(eventID, filteredEvents); + } catch (IllegalStateException ex) { + //Since the case is closed, the user probably doesn't care about this, just log it as a precaution. + LOGGER.log(Level.SEVERE, "There was no case open to lookup the Sleuthkit object backing a SingleEvent.", ex); // NON-NLS + return null; + } catch (TskCoreException ex) { + /* + * Just log it: There might be lots of these errors, and we + * don't want to flood the user with notifications. It will + * be obvious the UI is broken anyways + */ + LOGGER.log(Level.SEVERE, "Failed to lookup Sleuthkit object backing a SingleEvent.", ex); // NON-NLS return null; } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java index f61e2b5b28..9fb308229f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java @@ -62,13 +62,9 @@ public class TimeLineResultView { dataResultPanel = DataResultPanel.createInstanceUninitialized("", "", Node.EMPTY, 0, dataContent); //set up listeners on relevant properties - TimeLineController.getTimeZone().addListener((Observable observable) -> { - dataResultPanel.setPath(getSummaryString()); - }); + TimeLineController.getTimeZone().addListener(timeZone -> dataResultPanel.setPath(getSummaryString())); - controller.getSelectedEventIDs().addListener((Observable o) -> { - refresh(); - }); + controller.getSelectedEventIDs().addListener((Observable selectedIDs) -> refresh()); refresh(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListChart.java index 6f01fbbec0..e9b5f12d1a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListChart.java @@ -22,6 +22,7 @@ import java.util.Collection; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.TableCell; @@ -128,6 +129,15 @@ class ListChart extends BorderPane { table.getItems().setAll(eventIDs); } + /** + * Get the List of IDs of events that are selected in this list. + * + * @return The List of IDs of events that are selected in this list. + */ + ObservableList getSelectedEventIDs() { + return table.getSelectionModel().getSelectedItems(); + } + private class ImageCell extends EventTableCell { @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java index 6f3fdec60f..574de0fcfe 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.timeline.ui.listvew; import java.util.List; import javafx.application.Platform; +import javafx.beans.Observable; import javafx.concurrent.Task; import javafx.scene.Parent; import org.joda.time.Interval; @@ -58,6 +59,11 @@ public class ListViewPane extends AbstractTimeLineView { //initialize chart; setCenter(listChart); setSettingsNodes(new ListViewPane.ListViewSettingsPane().getChildrenUnmodifiable()); + + //keep controller's list of selected event IDs in sync with this list's + listChart.getSelectedEventIDs().addListener((Observable selectedIDs) -> { + controller.selectEventIDs(listChart.getSelectedEventIDs()); + }); } @Override