diff --git a/Core/src/org/sleuthkit/autopsy/core/layer.xml b/Core/src/org/sleuthkit/autopsy/core/layer.xml index 4c4b88125a..42315074d4 100644 --- a/Core/src/org/sleuthkit/autopsy/core/layer.xml +++ b/Core/src/org/sleuthkit/autopsy/core/layer.xml @@ -197,6 +197,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties index bf26cc976e..9f2e0c950c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties @@ -24,10 +24,7 @@ TimelinePanel.jButton7.text=3d TimelinePanel.jButton2.text=1m TimelinePanel.jButton3.text=3m TimelinePanel.jButton4.text=2w -TimeLineTopComponent.eventsTab.name=Events -TimeLineTopComponent.filterTab.name=Filters OpenTimelineAction.title=Timeline OpenTimeLineAction.msgdlg.text=Could not create timeline, there are no data sources. -TimeLineTopComponent.timeZonePanel.text=Display Times In\: ProgressWindow.progressHeader.text=\ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java b/Core/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java index 0c90af7f27..c0c00be083 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java @@ -42,8 +42,7 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined; * intended only to remove the boilerplate initialization code when defining a * relatively static layout * - * TODO: find a way to move this to CoreUtils and remove duplicate verison in - * image analyzer + * TODO: move this to CoreUtils and remove duplicate verison in image analyzer */ public class FXMLConstructor { @@ -55,12 +54,28 @@ public class FXMLConstructor { * * * @param node a node to initialize from a loaded FXML - * @param fxmlFileName the the file name of the FXML to load, relative to - * the package that the class of node is defined in. + * @param fxmlFileName the file name of the FXML to load, relative to the + * package that the class of node is defined in. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) static public void construct(Node node, String fxmlFileName) { - final String name = "nbres:/" + StringUtils.replace(node.getClass().getPackage().getName(), ".", "/") + "/" + fxmlFileName; // NON-NLS + construct(node, node.getClass(), fxmlFileName); + } + + /** + * Load an fxml file and initialize a node with it. Since this manipulates + * the node, it must be called on the JFX thread. + * + * + * @param node a node to initialize from a loaded FXML + * @param clazz a class to use for relative location of the fxml + * @param fxmlFileName the file name of the FXML to load, relative to the + * package of clazz. + * + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + static public void construct(Node node, Class clazz, String fxmlFileName) { + final String name = "nbres:/" + StringUtils.replace(clazz.getPackage().getName(), ".", "/") + "/" + fxmlFileName; // NON-NLS try { FXMLLoader fxmlLoader = new FXMLLoader(new URL(name)); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index e7dd27a635..4587e26122 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -64,13 +64,13 @@ import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; import static org.sleuthkit.autopsy.casemodule.Case.Events.DATA_SOURCE_ADDED; -import org.sleuthkit.autopsy.coreutils.History; -import org.sleuthkit.autopsy.coreutils.LoggedTask; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.coreutils.History; +import org.sleuthkit.autopsy.coreutils.LoggedTask; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; @@ -135,6 +135,21 @@ public class TimeLineController { private final ReadOnlyStringWrapper taskTitle = new ReadOnlyStringWrapper(); + private final ReadOnlyStringWrapper status = new ReadOnlyStringWrapper(); + + /** + * status is a string that will be displayed in the status bar as a kind of + * user hint/information when it is not empty + * + * @return the status property + */ + public ReadOnlyStringProperty getStatusProperty() { + return status.getReadOnlyProperty(); + } + + public void setStatus(String string) { + status.set(string); + } private final Case autoCase; @ThreadConfined(type = ThreadConfined.ThreadType.JFX) @@ -301,8 +316,10 @@ public class TimeLineController { LOGGER.log(Level.INFO, "Beginning generation of timeline"); // NON-NLS try { SwingUtilities.invokeLater(() -> { - if (isWindowOpen()) { - mainFrame.close(); + synchronized (TimeLineController.this) { + if (isWindowOpen()) { + mainFrame.close(); + } } }); final SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); @@ -347,8 +364,10 @@ public class TimeLineController { void rebuildTagsTable() { LOGGER.log(Level.INFO, "starting to rebuild tags table"); // NON-NLS SwingUtilities.invokeLater(() -> { - if (isWindowOpen()) { - mainFrame.close(); + synchronized (TimeLineController.this) { + if (isWindowOpen()) { + mainFrame.close(); + } } }); synchronized (eventsRepository) { @@ -373,16 +392,19 @@ public class TimeLineController { IngestManager.getInstance().removeIngestModuleEventListener(ingestModuleListener); IngestManager.getInstance().removeIngestJobEventListener(ingestJobListener); Case.removePropertyChangeListener(caseListener); - mainFrame.close(); - mainFrame.setVisible(false); - mainFrame = null; + SwingUtilities.invokeLater(() -> { + synchronized (TimeLineController.this) { + mainFrame.close(); + mainFrame = null; + } + }); } } /** * show the timeline window and prompt for rebuilding database if necessary. */ - synchronized void openTimeLine() { + void openTimeLine() { // listen for case changes (specifically images being added, and case changes). if (Case.isCaseOpen() && !listeningToAutopsy) { IngestManager.getInstance().addIngestModuleEventListener(ingestModuleListener); @@ -524,20 +546,20 @@ public class TimeLineController { /** * private method to build gui if necessary and make it visible. */ - synchronized private void showWindow() { - if (mainFrame == null) { - LOGGER.log(Level.WARNING, "Tried to show timeline with invalid window. Rebuilding GUI."); // NON-NLS - mainFrame = (TimeLineTopComponent) WindowManager.getDefault().findTopComponent( - NbBundle.getMessage(TimeLineController.class, "CTL_TimeLineTopComponentAction")); - if (mainFrame == null) { - mainFrame = new TimeLineTopComponent(); - } - mainFrame.setController(this); - } + private void showWindow() { SwingUtilities.invokeLater(() -> { - mainFrame.open(); - mainFrame.setVisible(true); - mainFrame.toFront(); + synchronized (TimeLineController.this) { + if (mainFrame == null) { + LOGGER.log(Level.WARNING, "Tried to show timeline with invalid window. Rebuilding GUI."); // NON-NLS + mainFrame = (TimeLineTopComponent) WindowManager.getDefault().findTopComponent( + NbBundle.getMessage(TimeLineController.class, "CTL_TimeLineTopComponentAction")); + if (mainFrame == null) { + mainFrame = new TimeLineTopComponent(this); + } + } + mainFrame.open(); + mainFrame.toFront(); + } }); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java index f19dc9bf1c..97821ce8e2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java @@ -23,7 +23,7 @@ import java.util.Collections; import java.util.List; import javafx.application.Platform; import javafx.beans.Observable; -import javafx.event.ActionEvent; +import javafx.embed.swing.JFXPanel; import javafx.scene.Scene; import javafx.scene.control.SplitPane; import javafx.scene.control.Tab; @@ -34,7 +34,6 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; -import org.netbeans.api.settings.ConvertAsProperties; import org.openide.explorer.ExplorerManager; import org.openide.explorer.ExplorerUtils; import org.openide.util.NbBundle; @@ -51,135 +50,103 @@ import org.sleuthkit.autopsy.timeline.ui.StatusBar; import org.sleuthkit.autopsy.timeline.ui.TimeLineResultView; import org.sleuthkit.autopsy.timeline.ui.TimeZonePanel; import org.sleuthkit.autopsy.timeline.ui.VisualizationPanel; -import org.sleuthkit.autopsy.timeline.ui.detailview.tree.NavPanel; +import org.sleuthkit.autopsy.timeline.ui.detailview.tree.EventsTree; import org.sleuthkit.autopsy.timeline.ui.filtering.FilterSetPanel; import org.sleuthkit.autopsy.timeline.zooming.ZoomSettingsPane; /** * TopComponent for the timeline feature. */ -@ConvertAsProperties( - dtd = "-//org.sleuthkit.autopsy.timeline//TimeLine//EN", - autostore = false) @TopComponent.Description( preferredID = "TimeLineTopComponent", //iconBase="SET/PATH/TO/ICON/HERE", persistenceType = TopComponent.PERSISTENCE_NEVER) @TopComponent.Registration(mode = "timeline", openAtStartup = false) -public final class TimeLineTopComponent extends TopComponent implements ExplorerManager.Provider, TimeLineUI { +public final class TimeLineTopComponent extends TopComponent implements ExplorerManager.Provider { private static final Logger LOGGER = Logger.getLogger(TimeLineTopComponent.class.getName()); - private DataContentPanel dataContentPanel; + private final DataContentPanel dataContentPanel; - private TimeLineResultView tlrv; + private final TimeLineResultView tlrv; private final ExplorerManager em = new ExplorerManager(); - private TimeLineController controller; + private final TimeLineController controller; - ////jfx componenets that make up the interface - private final FilterSetPanel filtersPanel = new FilterSetPanel(); - - private final Tab eventsTab = new Tab( - NbBundle.getMessage(TimeLineTopComponent.class, "TimeLineTopComponent.eventsTab.name")); - - private final Tab filterTab = new Tab( - NbBundle.getMessage(TimeLineTopComponent.class, "TimeLineTopComponent.filterTab.name")); - - private final VBox leftVBox = new VBox(5); - - private final NavPanel navPanel = new NavPanel(); - - private final StatusBar statusBar = new StatusBar(); - - private final TabPane tabPane = new TabPane(); - - private final ZoomSettingsPane zoomSettingsPane = new ZoomSettingsPane(); - - private final VisualizationPanel visualizationPanel = new VisualizationPanel(navPanel); - - private final SplitPane splitPane = new SplitPane(); - - private final TimeZonePanel timeZonePanel = new TimeZonePanel(); - - public TimeLineTopComponent() { + public TimeLineTopComponent(TimeLineController controller) { initComponents(); - + this.controller = controller; associateLookup(ExplorerUtils.createLookup(em, getActionMap())); setName(NbBundle.getMessage(TimeLineTopComponent.class, "CTL_TimeLineTopComponent")); setToolTipText(NbBundle.getMessage(TimeLineTopComponent.class, "HINT_TimeLineTopComponent")); setIcon(WindowManager.getDefault().getMainWindow().getIconImage()); //use the same icon as main application - timeZonePanel.setText(NbBundle.getMessage(this.getClass(), "TimeLineTopComponent.timeZonePanel.text")); - customizeComponents(); - } - - synchronized private void customizeComponents() { - dataContentPanel = DataContentPanel.createInstance(); this.contentViewerContainerPanel.add(dataContentPanel, BorderLayout.CENTER); - tlrv = new TimeLineResultView(dataContentPanel); + tlrv = new TimeLineResultView(controller, dataContentPanel); DataResultPanel dataResultPanel = tlrv.getDataResultPanel(); this.resultContainerPanel.add(dataResultPanel, BorderLayout.CENTER); dataResultPanel.open(); - - Platform.runLater(() -> { - //assemble ui componenets together - jFXstatusPanel.setScene(new Scene(statusBar)); - jFXVizPanel.setScene(new Scene(splitPane)); - - splitPane.setDividerPositions(0); - - filterTab.setClosable(false); - filterTab.setContent(filtersPanel); - filterTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/funnel.png")); // NON-NLS - - eventsTab.setClosable(false); - eventsTab.setContent(navPanel); - eventsTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/timeline_marker.png")); // NON-NLS - - tabPane.getTabs().addAll(filterTab, eventsTab); - VBox.setVgrow(tabPane, Priority.ALWAYS); - - VBox.setVgrow(timeZonePanel, Priority.SOMETIMES); - leftVBox.getChildren().addAll(timeZonePanel, zoomSettingsPane, tabPane); - - SplitPane.setResizableWithParent(leftVBox, Boolean.FALSE); - splitPane.getItems().addAll(leftVBox, visualizationPanel); - }); + customizeFXComponents(); } - @Override - public synchronized void setController(TimeLineController controller) { - this.controller = controller; - - tlrv.setController(controller); + @NbBundle.Messages({"TimeLineTopComponent.eventsTab.name=Events", + "TimeLineTopComponent.filterTab.name=Filters"}) + void customizeFXComponents() { Platform.runLater(() -> { - jFXVizPanel.getScene().addEventFilter(KeyEvent.KEY_PRESSED, - (KeyEvent event) -> { - if (new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN).match(event)) { - new Back(controller).handle(new ActionEvent()); - } else if (new KeyCodeCombination(KeyCode.BACK_SPACE).match(event)) { - new Back(controller).handle(new ActionEvent()); - } else if (new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN).match(event)) { - new Forward(controller).handle(new ActionEvent()); - } else if (new KeyCodeCombination(KeyCode.BACK_SPACE, KeyCodeCombination.SHIFT_DOWN).match(event)) { - new Forward(controller).handle(new ActionEvent()); - } - }); + + //create and wire up jfx componenets that make up the interface + final Tab filterTab = new Tab(Bundle.TimeLineTopComponent_filterTab_name(), new FilterSetPanel(controller)); + filterTab.setClosable(false); + filterTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/funnel.png")); // NON-NLS + + final EventsTree eventsTree = new EventsTree(controller); + final VisualizationPanel visualizationPanel = new VisualizationPanel(controller, eventsTree); + final Tab eventsTreeTab = new Tab(Bundle.TimeLineTopComponent_eventsTab_name(), eventsTree); + eventsTreeTab.setClosable(false); + eventsTreeTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/timeline_marker.png")); // NON-NLS + eventsTreeTab.disableProperty().bind(controller.viewModeProperty().isEqualTo(VisualizationMode.COUNTS)); + + final TabPane leftTabPane = new TabPane(filterTab, eventsTreeTab); + VBox.setVgrow(leftTabPane, Priority.ALWAYS); controller.viewModeProperty().addListener((Observable observable) -> { if (controller.viewModeProperty().get().equals(VisualizationMode.COUNTS)) { - tabPane.getSelectionModel().select(filterTab); + //if view mode is counts, make sure events tabd is not active + leftTabPane.getSelectionModel().select(filterTab); } }); - eventsTab.disableProperty().bind(controller.viewModeProperty().isEqualTo(VisualizationMode.COUNTS)); - visualizationPanel.setController(controller); - navPanel.setController(controller); - filtersPanel.setController(controller); - zoomSettingsPane.setController(controller); - statusBar.setController(controller); + + final TimeZonePanel timeZonePanel = new TimeZonePanel(); + VBox.setVgrow(timeZonePanel, Priority.SOMETIMES); + + final ZoomSettingsPane zoomSettingsPane = new ZoomSettingsPane(controller); + + final VBox leftVBox = new VBox(5, timeZonePanel, zoomSettingsPane, leftTabPane); + SplitPane.setResizableWithParent(leftVBox, Boolean.FALSE); + + final SplitPane mainSplitPane = new SplitPane(leftVBox, visualizationPanel); + mainSplitPane.setDividerPositions(0); + + final Scene scene = new Scene(mainSplitPane); + scene.addEventFilter(KeyEvent.KEY_PRESSED, + (KeyEvent event) -> { + if (new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN).match(event)) { + new Back(controller).handle(null); + } else if (new KeyCodeCombination(KeyCode.BACK_SPACE).match(event)) { + new Back(controller).handle(null); + } else if (new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN).match(event)) { + new Forward(controller).handle(null); + } else if (new KeyCodeCombination(KeyCode.BACK_SPACE, KeyCodeCombination.SHIFT_DOWN).match(event)) { + new Forward(controller).handle(null); + } + }); + + //add ui componenets to JFXPanels + jFXVizPanel.setScene(scene); + jFXstatusPanel.setScene(new Scene(new StatusBar(controller))); + }); } @@ -196,9 +163,9 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer // //GEN-BEGIN:initComponents private void initComponents() { - jFXstatusPanel = new javafx.embed.swing.JFXPanel(); + jFXstatusPanel = new JFXPanel(); splitYPane = new javax.swing.JSplitPane(); - jFXVizPanel = new javafx.embed.swing.JFXPanel(); + jFXVizPanel = new JFXPanel(); lowerSplitXPane = new javax.swing.JSplitPane(); resultContainerPanel = new javax.swing.JPanel(); contentViewerContainerPanel = new javax.swing.JPanel(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineUI.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineUI.java deleted file mode 100644 index bb14f76aee..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineUI.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2014 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; - -/** - * - */ -public interface TimeLineUI { - - void setController(TimeLineController controller); -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineView.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineView.java deleted file mode 100644 index 4c880c9cf0..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineView.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013 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; - -/** - * Interface to be implemented by views of the data. - * - * Most implementations should install the relevant listeners in their - * {@link #setController} and {@link #setModel} methods - */ -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; - -public interface TimeLineView extends TimeLineUI { - - @Override - void setController(TimeLineController controller); - - void setModel(final FilteredEventsModel filteredEvents); - -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/actions/Bundle.properties index 89f959f6a4..1140e0d19b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/Bundle.properties @@ -1,6 +1,4 @@ Back.actions.name.text=Back DefaultFilters.action.name.text=apply default filters Forward.action.name.text=Forward -SaveSnapshot.action.name.text=save snapshot -SaveSnapshot.fileChoose.title.text=Save snapshot to -ZoomOut.action.name.text=apply default filters \ No newline at end of file + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java similarity index 71% rename from Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java rename to Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java index bc11657244..8faa1bfe53 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java @@ -46,51 +46,48 @@ import org.sleuthkit.datamodel.TskCoreException; /** */ -public class SaveSnapshot extends Action { +public class SaveSnapshotAsReport extends Action { private static final String HTML_EXT = ".html"; - private static final String REPORT_IMAGE_EXTENSION = ".png"; - private static final Logger LOGGER = Logger.getLogger(SaveSnapshot.class.getName()); + private static final Logger LOGGER = Logger.getLogger(SaveSnapshotAsReport.class.getName()); - private final TimeLineController controller; - - private final WritableImage snapshot; - - public SaveSnapshot(TimeLineController controller, WritableImage snapshot) { - super(NbBundle.getMessage(SaveSnapshot.class, "SaveSnapshot.action.name.text")); - this.controller = controller; - this.snapshot = snapshot; + @NbBundle.Messages({"SaveSnapshot.action.name.text=save snapshot", + "SaveSnapshot.fileChoose.title.text=Save snapshot to"}) + public SaveSnapshotAsReport(TimeLineController controller, WritableImage snapshot) { + super(Bundle.SaveSnapshot_action_name_text()); setEventHandler(new Consumer() { @Override public void accept(ActionEvent t) { //choose location/name DirectoryChooser fileChooser = new DirectoryChooser(); - fileChooser.setTitle(NbBundle.getMessage(this.getClass(), "SaveSnapshot.fileChoose.title.text")); + fileChooser.setTitle(Bundle.SaveSnapshot_fileChoose_title_text()); fileChooser.setInitialDirectory(new File(Case.getCurrentCase().getReportDirectory())); - File outFolder = fileChooser.showDialog(null); - if (outFolder == null) { + File reportDirectory = fileChooser.showDialog(null); + if (reportDirectory == null) { return; } - outFolder.mkdir(); - String name = outFolder.getName(); + reportDirectory.mkdir(); + String reportName = reportDirectory.getName(); + String reportPath = reportDirectory.getPath(); //gather metadata List> reportMetaData = new ArrayList<>(); reportMetaData.add(new Pair<>("Case", Case.getCurrentCase().getName())); // NON-NLS - ZoomParams get = controller.getEventsModel().zoomParametersProperty().get(); - reportMetaData.add(new Pair<>("Time Range", get.getTimeRange().toString())); // NON-NLS - reportMetaData.add(new Pair<>("Description Level of Detail", get.getDescriptionLOD().getDisplayName())); // NON-NLS - reportMetaData.add(new Pair<>("Event Type Zoom Level", get.getTypeZoomLevel().getDisplayName())); // NON-NLS - reportMetaData.add(new Pair<>("Filters", get.getFilter().getHTMLReportString())); // NON-NLS + ZoomParams zoomParams = controller.getEventsModel().zoomParametersProperty().get(); + reportMetaData.add(new Pair<>("Time Range", zoomParams.getTimeRange().toString())); // NON-NLS + reportMetaData.add(new Pair<>("Description Level of Detail", zoomParams.getDescriptionLOD().getDisplayName())); // NON-NLS + reportMetaData.add(new Pair<>("Event Type Zoom Level", zoomParams.getTypeZoomLevel().getDisplayName())); // NON-NLS + reportMetaData.add(new Pair<>("Filters", zoomParams.getFilter().getHTMLReportString())); // NON-NLS //save snapshot as png try { - ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", new File(outFolder.getPath() + File.separator + outFolder.getName() + REPORT_IMAGE_EXTENSION)); // NON-NLS + ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", + new File(reportPath, reportName + REPORT_IMAGE_EXTENSION)); // NON-NLS } catch (IOException ex) { LOGGER.log(Level.WARNING, "failed to write snapshot to disk", ex); // NON-NLS return; @@ -99,17 +96,18 @@ public class SaveSnapshot extends Action { //build html string StringBuilder wrapper = new StringBuilder(); wrapper.append("\n\n\t").append("timeline snapshot").append("\n\t\n\n\n"); // NON-NLS - wrapper.append("
\n

").append(outFolder.getName()).append("

\n"); // NON-NLS - wrapper.append("\"snaphot\""); // NON-NLS + wrapper.append("
\n

").append(reportDirectory.getName()).append("

\n"); // NON-NLS + wrapper.append("\"snaphot\""); // NON-NLS wrapper.append("\n"); // NON-NLS for (Pair pair : reportMetaData) { wrapper.append("\n"); // NON-NLS } wrapper.append("
").append(pair.getKey()).append(": ").append(pair.getValue()).append("
\n"); // NON-NLS wrapper.append("
\n\n"); // NON-NLS + File reportHTMLFIle = new File(reportDirectory, reportName + HTML_EXT); //write html wrapper - try (Writer htmlWriter = new FileWriter(new File(outFolder, name + HTML_EXT))) { + try (Writer htmlWriter = new FileWriter(reportHTMLFIle)) { htmlWriter.write(wrapper.toString()); } catch (FileNotFoundException ex) { LOGGER.log(Level.WARNING, "failed to open html wrapper file for writing ", ex); // NON-NLS @@ -121,14 +119,14 @@ public class SaveSnapshot extends Action { //copy css try (InputStream resource = this.getClass().getResourceAsStream("/org/sleuthkit/autopsy/timeline/index.css")) { // NON-NLS - Files.copy(resource, Paths.get(outFolder.getPath(), "index.css")); // NON-NLS + Files.copy(resource, Paths.get(reportPath, "index.css")); // NON-NLS } catch (IOException ex) { LOGGER.log(Level.WARNING, "failed to copy css file", ex); // NON-NLS } //add html file as report to case try { - Case.getCurrentCase().addReport(outFolder.getPath() + File.separator + outFolder.getName() + HTML_EXT, "Timeline", outFolder.getName() + HTML_EXT); // NON-NLS + Case.getCurrentCase().addReport(reportHTMLFIle.getPath(), "Timeline", reportName + HTML_EXT); // NON-NLS } catch (TskCoreException ex) { LOGGER.log(Level.WARNING, "failed add html wrapper as a report", ex); // NON-NLS } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomIn.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomIn.java new file mode 100644 index 0000000000..d1f76c3c7e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomIn.java @@ -0,0 +1,44 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.actions; + +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import org.controlsfx.control.action.Action; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.timeline.TimeLineController; + +/** + * + */ +public class ZoomIn extends Action { + + private static final Image MAGNIFIER_IN = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in-green.png"); //NOI18N + + @NbBundle.Messages({"ZoomIn.longText=Zoom in to view half as much time.", + "ZoomIn.action.text=Zoom in"}) + public ZoomIn(TimeLineController controller) { + super(Bundle.ZoomIn_action_text()); + setLongText(Bundle.ZoomIn_longText()); + setGraphic(new ImageView(MAGNIFIER_IN)); + setEventHandler(actionEvent -> { + controller.pushZoomInTime(); + }); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java index ee362dfc40..8818ff4557 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,8 @@ package org.sleuthkit.autopsy.timeline.actions; import javafx.beans.binding.BooleanBinding; -import javafx.event.ActionEvent; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import org.controlsfx.control.action.Action; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.TimeLineController; @@ -30,15 +31,22 @@ import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; */ public class ZoomOut extends Action { - private final TimeLineController controller; + private static final Image MAGNIFIER_OUT = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png"); //NOI18N - private final FilteredEventsModel eventsModel; + @NbBundle.Messages({"ZoomOut.longText=Zoom out to view 50% more time.", + "ZoomOut.action.text=Zoom out"}) + public ZoomOut(TimeLineController controller) { + super(Bundle.ZoomOut_action_text()); + setLongText(Bundle.ZoomOut_longText()); + setGraphic(new ImageView(MAGNIFIER_OUT)); + setEventHandler(actionEvent -> { + controller.pushZoomOutTime(); + }); - public ZoomOut(final TimeLineController controller) { - super(NbBundle.getMessage(ZoomOut.class, "ZoomOut.action.name.text")); - this.controller = controller; - eventsModel = controller.getEventsModel(); + //disable action when the current time range already encompases the entire case. disabledProperty().bind(new BooleanBinding() { + private final FilteredEventsModel eventsModel = controller.getEventsModel(); + { bind(eventsModel.zoomParametersProperty()); } @@ -48,8 +56,5 @@ public class ZoomOut extends Action { return eventsModel.zoomParametersProperty().getValue().getTimeRange().contains(eventsModel.getSpanningInterval()); } }); - setEventHandler((ActionEvent t) -> { - controller.zoomOutToActivity(); - }); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomToEvents.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomToEvents.java new file mode 100644 index 0000000000..7bcc076936 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomToEvents.java @@ -0,0 +1,61 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014-15 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.actions; + +import javafx.beans.binding.BooleanBinding; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import org.controlsfx.control.action.Action; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; + +/** + * + */ +public class ZoomToEvents extends Action { + + private static final Image MAGNIFIER_OUT = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png", 16, 16, true, true); //NOI18N + + @NbBundle.Messages({"ZoomToEvents.action.text=Zoom to events", + "ZoomToEvents.longText=Zoom out to show the nearest events."}) + public ZoomToEvents(final TimeLineController controller) { + super(Bundle.ZoomToEvents_action_text()); + setLongText(Bundle.ZoomToEvents_longText()); + setGraphic(new ImageView(MAGNIFIER_OUT)); + setEventHandler(actionEvent -> { + controller.zoomOutToActivity(); + }); + + //disable action when the current time range already encompases the entire case. + disabledProperty().bind(new BooleanBinding() { + private final FilteredEventsModel eventsModel = controller.getEventsModel(); + + { + bind(eventsModel.zoomParametersProperty()); + } + + @Override + protected boolean computeValue() { + //TODO: do a db query to see if using this action will actually result in viewable events + return eventsModel.zoomParametersProperty().getValue().getTimeRange().contains(eventsModel.getSpanningInterval()); + } + }); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index ae354f87f8..f34c46fc64 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -41,7 +41,6 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent.DeletedContentTagInfo; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.timeline.TimeLineView; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType; import org.sleuthkit.autopsy.timeline.db.EventsRepository; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/cross-script.png b/Core/src/org/sleuthkit/autopsy/timeline/images/cross-script.png new file mode 100644 index 0000000000..f37cf41830 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/cross-script.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/information-gray.png b/Core/src/org/sleuthkit/autopsy/timeline/images/information-gray.png new file mode 100644 index 0000000000..c0dd006c9f Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/information-gray.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/select.png b/Core/src/org/sleuthkit/autopsy/timeline/images/select.png new file mode 100644 index 0000000000..a8c5583a7f Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/select.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java similarity index 88% rename from Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java rename to Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index 993f61501e..8d16c7a86f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -40,6 +40,7 @@ import javafx.scene.chart.Chart; import javafx.scene.chart.XYChart; import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; +import javafx.scene.control.Tooltip; import javafx.scene.effect.Effect; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; @@ -50,12 +51,11 @@ import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; -import javax.annotation.Nonnull; import javax.annotation.concurrent.Immutable; import org.apache.commons.lang3.StringUtils; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; -import org.sleuthkit.autopsy.timeline.TimeLineView; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; @@ -73,8 +73,15 @@ import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; * {@link XYChart} doing the rendering. Is this a good idea? -jm TODO: pull up * common history context menu items out of derived classes? -jm */ -public abstract class AbstractVisualization & TimeLineChart> extends BorderPane implements TimeLineView { +public abstract class AbstractVisualizationPane & TimeLineChart> extends BorderPane { + @NbBundle.Messages("AbstractVisualization.Drag_Tooltip.text=Drag the mouse to select a time interval to zoom into.") + private static final Tooltip DRAG_TOOLTIP = new Tooltip(Bundle.AbstractVisualization_Drag_Tooltip_text()); + private static final Logger LOGGER = Logger.getLogger(AbstractVisualizationPane.class.getName()); + + public static Tooltip getDragTooltip() { + return DRAG_TOOLTIP; + } protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true); protected final ObservableList> dataSets = FXCollections.>observableArrayList(); @@ -93,11 +100,15 @@ public abstract class AbstractVisualization & T */ private Task updateTask; - protected TimeLineController controller; + final protected TimeLineController controller; - protected FilteredEventsModel filteredEvents; + final protected FilteredEventsModel filteredEvents; - protected ReadOnlyListWrapper selectedNodes = new ReadOnlyListWrapper<>(FXCollections.observableArrayList()); + final protected ReadOnlyListWrapper selectedNodes = new ReadOnlyListWrapper<>(FXCollections.observableArrayList()); + + private InvalidationListener invalidationListener = (Observable observable) -> { + update(); + }; public ReadOnlyListProperty getSelectedNodes() { return selectedNodes.getReadOnlyProperty(); @@ -177,7 +188,7 @@ public abstract class AbstractVisualization & T * Primarily this invokes the background {@link Task} returned by * {@link #getUpdateTask()} which derived classes must implement. */ - synchronized public void update() { + final synchronized public void update() { if (updateTask != null) { updateTask.cancel(true); updateTask = null; @@ -195,7 +206,7 @@ public abstract class AbstractVisualization & T try { this.hasEvents.set(updateTask.get()); } catch (InterruptedException | ExecutionException ex) { - Logger.getLogger(AbstractVisualization.class.getName()).log(Level.SEVERE, "Unexpected exception updating visualization", ex); + LOGGER.log(Level.SEVERE, "Unexpected exception updating visualization", ex); //NOI18N } break; } @@ -203,7 +214,7 @@ public abstract class AbstractVisualization & T controller.monitorTask(updateTask); } - synchronized public void dispose() { + final synchronized public void dispose() { if (updateTask != null) { updateTask.cancel(true); } @@ -211,7 +222,12 @@ public abstract class AbstractVisualization & T invalidationListener = null; } - protected AbstractVisualization(Pane partPane, Pane contextPane, Region spacer) { + protected AbstractVisualizationPane(TimeLineController controller, Pane partPane, Pane contextPane, Region spacer) { + this.controller = controller; + + this.filteredEvents = controller.getEventsModel(); + this.filteredEvents.registerForEvents(this); + this.filteredEvents.zoomParametersProperty().addListener(invalidationListener); this.leafPane = partPane; this.branchPane = contextPane; this.spacer = spacer; @@ -226,32 +242,18 @@ public abstract class AbstractVisualization & T }); } }); - } - @Override - synchronized public void setController(TimeLineController controller) { - this.controller = controller; - chart.setController(controller); - - setModel(controller.getEventsModel()); - TimeLineController.getTimeZone().addListener((Observable observable) -> { - update(); + TimeLineController.getTimeZone().addListener(invalidationListener); + + //show tooltip text in status bar + hoverProperty().addListener((observable, oldActivated, newActivated) -> { + if (newActivated) { + controller.setStatus(DRAG_TOOLTIP.getText()); + } else { + controller.setStatus(""); + } }); - } - - @Override - synchronized public void setModel(@Nonnull FilteredEventsModel filteredEvents) { - - if (this.filteredEvents != null && this.filteredEvents != filteredEvents) { - this.filteredEvents.unRegisterForEvents(this); - this.filteredEvents.zoomParametersProperty().removeListener(invalidationListener); - } - if (this.filteredEvents != filteredEvents) { - filteredEvents.registerForEvents(this); - filteredEvents.zoomParametersProperty().addListener(invalidationListener); - } - this.filteredEvents = filteredEvents; - + update(); } @@ -260,10 +262,6 @@ public abstract class AbstractVisualization & T update(); } - protected InvalidationListener invalidationListener = (Observable observable) -> { - update(); - }; - /** * iterate through the list of tick-marks building a two level structure of * replacement tick marl labels. (Visually) upper level has most @@ -370,7 +368,7 @@ public abstract class AbstractVisualization & T */ private synchronized void assignLeafLabel(String labelText, double labelWidth, double labelX, boolean bold) { - Text label = new Text(" " + labelText + " "); + Text label = new Text(" " + labelText + " "); //NOI18N label.setTextAlignment(TextAlignment.CENTER); label.setFont(Font.font(null, bold ? FontWeight.BOLD : FontWeight.NORMAL, 10)); //position label accounting for width @@ -414,9 +412,9 @@ public abstract class AbstractVisualization & T label.relocate(labelX, 0); if (labelX == 0) { // first label has no border - label.setStyle("-fx-border-width: 0 0 0 0 ; -fx-border-color:black;"); // NON-NLS + label.setStyle("-fx-border-width: 0 0 0 0 ; -fx-border-color:black;"); // NON-NLS //NOI18N } else { // subsequent labels have border on left to create dividers - label.setStyle("-fx-border-width: 0 0 0 1; -fx-border-color:black;"); // NON-NLS + label.setStyle("-fx-border-width: 0 0 0 1; -fx-border-color:black;"); // NON-NLS //NOI18N } branchPane.getChildren().add(label); @@ -446,10 +444,10 @@ public abstract class AbstractVisualization & T TwoPartDateTime(String dateString) { //find index of separator to spit on - int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":"); + int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":"); //NOI18N if (splitIndex < 0) { // there is only one part leaf = dateString; - branch = ""; + branch = ""; //NOI18N } else { //split at index leaf = StringUtils.substring(dateString, splitIndex + 1); branch = StringUtils.substring(dateString, 0, splitIndex); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties index 1c31801521..521cf76e4f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,7 +35,6 @@ Timeline.ui.ZoomRanges.threeyears.text=Three Years Timeline.ui.ZoomRanges.fiveyears.text=Five Years Timeline.ui.ZoomRanges.tenyears.text=Ten Years Timeline.ui.ZoomRanges.all.text=All -Timeline.ui.TimeLineChart.tooltip.text=Double-click to zoom into range\:\n{0} to {1}\nRight-click to clear. TimeLineResultView.startDateToEndDate.text={0} to {1} VisualizationPanel.histogramTask.title=Rebuild Histogram VisualizationPanel.histogramTask.preparing=preparing @@ -56,4 +55,4 @@ VisualizationPanel.zoomMenuButton.text=Zoom in/out to VisualizationPanel.snapShotButton.text=Screenshot VisualizationPanel.detailsToggle.text=Details VisualizationPanel.countsToggle.text=Counts -VisualizationPanel.resetFiltersButton.text=Reset all filters +VisualizationPanel.resetFiltersButton.text=Reset all filters \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.css b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.css new file mode 100644 index 0000000000..b349466632 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.css @@ -0,0 +1,16 @@ +.intervalSelector{ + -fx-background-color: rgba(0,0,255,.25); + -fx-border-color: rgba(0,0,255,.25); + -fx-border-width: 0 3 0 3; +} + +.closeButton{ +} + +.closeButton:hover{ + -fx-opacity: 1; +} + +.zoomButton:hover{ + -fx-opacity: 1; +} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml new file mode 100644 index 0000000000..a3885619d4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java new file mode 100644 index 0000000000..197914167a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java @@ -0,0 +1,316 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014-15 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.ui; + +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.geometry.Point2D; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import org.controlsfx.control.action.Action; +import org.controlsfx.control.action.ActionUtils; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.timeline.FXMLConstructor; +import org.sleuthkit.autopsy.timeline.TimeLineController; + +/** + * Visually represents a 'selected' time range, and allows mouse interactions + * with it. + * + * @param the type of values along the x axis this is a selector for + * + * This abstract class requires concrete implementations to implement template + * methods to handle formating and date 'lookup' of the generic x-axis type + */ +public abstract class IntervalSelector extends BorderPane { + + private static final Image ClEAR_INTERVAL_ICON = new Image("/org/sleuthkit/autopsy/timeline/images/cross-script.png", 16, 16, true, true, true); + private static final Image ZOOM_TO_INTERVAL_ICON = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png", 16, 16, true, true, true); + private static final double STROKE_WIDTH = 3; + private static final double HALF_STROKE = STROKE_WIDTH / 2; + + /** + * the Axis this is a selector over + */ + public final TimeLineChart chart; + + private Tooltip tooltip; + /////////drag state + private DragPosition dragPosition; + private double startLeft; + private double startDragX; + private double startWidth; + + private final BooleanProperty isDragging = new SimpleBooleanProperty(false); + /////////end drag state + private final TimeLineController controller; + + @FXML + private Label startLabel; + + @FXML + private Label endLabel; + + @FXML + private Button closeButton; + + @FXML + private Button zoomButton; + + @FXML + private BorderPane bottomBorder; + + public IntervalSelector(TimeLineChart chart) { + this.chart = chart; + this.controller = chart.getController(); + FXMLConstructor.construct(this, IntervalSelector.class, "IntervalSelector.fxml"); // NON-NLS + } + + @FXML + void initialize() { + assert startLabel != null : "fx:id=\"startLabel\" was not injected: check your FXML file 'IntervalSelector.fxml'."; + assert endLabel != null : "fx:id=\"endLabel\" was not injected: check your FXML file 'IntervalSelector.fxml'."; + assert closeButton != null : "fx:id=\"closeButton\" was not injected: check your FXML file 'IntervalSelector.fxml'."; + assert zoomButton != null : "fx:id=\"zoomButton\" was not injected: check your FXML file 'IntervalSelector.fxml'."; + + setMaxHeight(USE_PREF_SIZE); + setMinHeight(USE_PREF_SIZE); + setMaxWidth(USE_PREF_SIZE); + setMinWidth(USE_PREF_SIZE); + + BooleanBinding showingControls = hoverProperty().and(isDragging.not()); + closeButton.visibleProperty().bind(showingControls); + closeButton.managedProperty().bind(showingControls); + zoomButton.visibleProperty().bind(showingControls); + zoomButton.managedProperty().bind(showingControls); + + widthProperty().addListener(o -> { + IntervalSelector.this.updateStartAndEnd(); + if (startLabel.getWidth() + zoomButton.getWidth() + endLabel.getWidth() > getWidth()) { + this.setCenter(zoomButton); + } else { + bottomBorder.setCenter(zoomButton); + } + BorderPane.setAlignment(zoomButton, Pos.BOTTOM_CENTER); + }); + layoutXProperty().addListener(o -> this.updateStartAndEnd()); + updateStartAndEnd(); + + setOnMouseMoved(mouseMove -> { + Point2D parentMouse = getLocalMouseCoords(mouseMove); + final double diffX = getLayoutX() - parentMouse.getX(); + if (Math.abs(diffX) <= HALF_STROKE) { + setCursor(Cursor.W_RESIZE); + } else if (Math.abs(diffX + getWidth()) <= HALF_STROKE) { + setCursor(Cursor.E_RESIZE); + } else { + setCursor(Cursor.HAND); + } + mouseMove.consume(); + }); + + setOnMousePressed(mousePress -> { + Point2D parentMouse = getLocalMouseCoords(mousePress); + final double diffX = getLayoutX() - parentMouse.getX(); + startDragX = mousePress.getScreenX(); + startWidth = getWidth(); + startLeft = getLayoutX(); + if (Math.abs(diffX) <= HALF_STROKE) { + dragPosition = IntervalSelector.DragPosition.LEFT; + } else if (Math.abs(diffX + getWidth()) <= HALF_STROKE) { + dragPosition = IntervalSelector.DragPosition.RIGHT; + } else { + dragPosition = IntervalSelector.DragPosition.CENTER; + } + mousePress.consume(); + }); + + setOnMouseReleased(mouseRelease -> isDragging.set(false)); + setOnMouseDragged(mouseDrag -> { + isDragging.set(true); + double dX = mouseDrag.getScreenX() - startDragX; + switch (dragPosition) { + case CENTER: + setLayoutX(startLeft + dX); + break; + case LEFT: + if (dX > startWidth) { + startDragX = mouseDrag.getScreenX(); + startWidth = 0; + dragPosition = DragPosition.RIGHT; + } else { + setLayoutX(startLeft + dX); + setPrefWidth(startWidth - dX); + autosize(); + } + break; + case RIGHT: + Point2D parentMouse = getLocalMouseCoords(mouseDrag); + if (parentMouse.getX() < startLeft) { + dragPosition = DragPosition.LEFT; + startDragX = mouseDrag.getScreenX(); + startWidth = 0; + } else { + setPrefWidth(startWidth + dX); + autosize(); + } + break; + } + mouseDrag.consume(); + }); + + ActionUtils.configureButton(new ZoomToSelectedIntervalAction(), zoomButton); + ActionUtils.configureButton(new ClearSelectedIntervalAction(), closeButton); + + //have to add handler rather than use convenience methods so that charts can listen for dismisal click + setOnMouseClicked(mosueClick -> { + if (mosueClick.getButton() == MouseButton.SECONDARY) { + chart.clearIntervalSelector(); + mosueClick.consume(); + } + if (mosueClick.getClickCount() >= 2) { + zoomToSelectedInterval(); + mosueClick.consume(); + } + }); + } + + private Point2D getLocalMouseCoords(MouseEvent mouseEvent) { + return getParent().sceneToLocal(new Point2D(mouseEvent.getSceneX(), mouseEvent.getSceneY())); + } + + private void zoomToSelectedInterval() { + //convert to DateTimes, using max/min if null(off axis) + DateTime start = parseDateTime(getSpanStart()); + DateTime end = parseDateTime(getSpanEnd()); + Interval i = adjustInterval(start.isBefore(end) ? new Interval(start, end) : new Interval(end, start)); + controller.pushTimeRange(i); + } + + /** + * + * @param i the interval represented by this selector + * + * @return a modified version of {@code i} adjusted to suite the needs of + * the concrete implementation + */ + protected abstract Interval adjustInterval(Interval i); + + /** + * format a string representation of the given x-axis value to use in the + * tooltip + * + * @param date a x-axis value of type X + * + * @return a string representation of the given x-axis value + */ + protected abstract String formatSpan(final X date); + + /** + * parse an x-axis value to a {@link DateTime} + * + * @param date a x-axis value of type X + * + * @return a {@link DateTime} corresponding to the given x-axis value + */ + protected abstract DateTime parseDateTime(X date); + + @NbBundle.Messages(value = {"# {0} - start timestamp", + "# {1} - end timestamp", + "Timeline.ui.TimeLineChart.tooltip.text=Double-click to zoom into range:\n{0} to {1}\nRight-click to clear."}) + private void updateStartAndEnd() { + String startString = formatSpan(getSpanStart()); + String endString = formatSpan(getSpanEnd()); + startLabel.setText(startString); + endLabel.setText(endString); + + Tooltip.uninstall(this, tooltip); + tooltip = new Tooltip(Bundle.Timeline_ui_TimeLineChart_tooltip_text(startString, endString)); + Tooltip.install(this, tooltip); + } + + /** + * @return the value along the x-axis corresponding to the left edge of the + * selector + */ + public X getSpanEnd() { + return getValueForDisplay(getBoundsInParent().getMaxX()); + } + + /** + * @return the value along the x-axis corresponding to the right edge of the + * selector + */ + public X getSpanStart() { + return getValueForDisplay(getBoundsInParent().getMinX()); + } + + private X getValueForDisplay(final double display) { + return chart.getXAxis().getValueForDisplay(chart.getXAxis().parentToLocal(display, 0).getX()); + } + + /** + * enum to represent whether the drag is a left/right-edge modification or a + * horizontal slide triggered by dragging the center + */ + private enum DragPosition { + + LEFT, + CENTER, + RIGHT + } + + private class ZoomToSelectedIntervalAction extends Action { + + @NbBundle.Messages("IntervalSelector.ZoomAction.name=Zoom") + ZoomToSelectedIntervalAction() { + super(Bundle.IntervalSelector_ZoomAction_name()); + setGraphic(new ImageView(ZOOM_TO_INTERVAL_ICON)); + setEventHandler((ActionEvent t) -> { + zoomToSelectedInterval(); + }); + } + } + + private class ClearSelectedIntervalAction extends Action { + + @NbBundle.Messages("IntervalSelector.ClearSelectedIntervalAction.tooltTipText=Clear Selected Interval") + ClearSelectedIntervalAction() { + super(""); + setLongText(Bundle.IntervalSelector_ClearSelectedIntervalAction_tooltTipText()); + setGraphic(new ImageView(ClEAR_INTERVAL_ICON)); + setEventHandler((ActionEvent t) -> { + chart.clearIntervalSelector(); + }); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml index 781bb18d1a..a731fb13a6 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml @@ -5,7 +5,7 @@ - + + - -