/* * Autopsy Forensic Browser * * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.timeline; import java.beans.PropertyVetoException; 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.scene.Scene; import javafx.scene.control.SplitPane; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javax.swing.JComponent; import javax.swing.SwingUtilities; import org.controlsfx.control.Notifications; import org.joda.time.Interval; import org.joda.time.format.DateTimeFormatter; import org.openide.explorer.ExplorerManager; import org.openide.explorer.ExplorerUtils; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.util.NbBundle; import org.openide.windows.Mode; import org.openide.windows.TopComponent; import static org.openide.windows.TopComponent.PROP_UNDOCKING_DISABLED; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.actions.AddBookmarkTagAction; import org.sleuthkit.autopsy.corecomponents.DataContentPanel; import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; 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.explorernodes.EventRootNode; import org.sleuthkit.autopsy.timeline.ui.HistoryToolBar; import org.sleuthkit.autopsy.timeline.ui.StatusBar; import org.sleuthkit.autopsy.timeline.ui.TimeZonePanel; import org.sleuthkit.autopsy.timeline.ui.ViewFrame; 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.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 { private static final Logger LOGGER = Logger.getLogger(TimeLineTopComponent.class.getName()); @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private final DataContentPanel contentViewerPanel; @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private DataResultPanel dataResultPanel; @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private final ExplorerManager em = new ExplorerManager(); private final TimeLineController controller; /** * Listener that drives the result viewer or content viewer (depending on * view mode) according to the controller's selected event IDs */ @NbBundle.Messages({"TimelineTopComponent.selectedEventListener.errorMsg=There was a problem getting the content for the selected event."}) private final InvalidationListener selectedEventsListener = new InvalidationListener() { @Override public void invalidated(Observable observable) { List<Long> selectedEventIDs = controller.getSelectedEventIDs(); //depending on the active view mode, we either update the dataResultPanel, or update the contentViewerPanel directly. switch (controller.getViewMode()) { case LIST: //make an array of EventNodes for the selected events EventNode[] childArray = new EventNode[selectedEventIDs.size()]; try { for (int i = 0; i < selectedEventIDs.size(); i++) { childArray[i] = EventNode.createEventNode(selectedEventIDs.get(i), controller.getEventsModel()); } Children children = new Children.Array(); children.add(childArray); SwingUtilities.invokeLater(() -> { //set generic container node as root context em.setRootContext(new AbstractNode(children)); try { //set selected nodes for actions em.setSelectedNodes(childArray); } catch (PropertyVetoException ex) { //I don't know why this would ever happen. LOGGER.log(Level.SEVERE, "Selecting the event node was vetoed.", ex); // NON-NLS } //if there is only one event selected push it into content viewer. if (childArray.length == 1) { contentViewerPanel.setNode(childArray[0]); } else { contentViewerPanel.setNode(null); } }); } 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(jFXViewPanel.getScene().getWindow()) .text(Bundle.TimelineTopComponent_selectedEventListener_errorMsg()) .showError(); }); } break; case COUNTS: case DETAIL: //make a root node with nodes for the selected events as children and push it to the result viewer. EventRootNode rootNode = new EventRootNode(selectedEventIDs, controller.getEventsModel()); SwingUtilities.invokeLater(() -> { dataResultPanel.setPath(getResultViewerSummaryString()); dataResultPanel.setNode(rootNode); }); break; default: throw new UnsupportedOperationException("Unknown view mode: " + controller.getViewMode()); } } }; private void syncViewMode() { switch (controller.getViewMode()) { case COUNTS: case DETAIL: /* * For counts and details mode, restore the result table at the * bottom left. */ SwingUtilities.invokeLater(() -> { splitYPane.remove(contentViewerPanel); if ((horizontalSplitPane.getParent() == splitYPane) == false) { splitYPane.setBottomComponent(horizontalSplitPane); horizontalSplitPane.setRightComponent(contentViewerPanel); } }); break; case LIST: /* * For list mode, remove the result table, and let the content * viewer expand across the bottom. */ SwingUtilities.invokeLater(() -> { splitYPane.setBottomComponent(contentViewerPanel); }); break; default: throw new UnsupportedOperationException("Unknown ViewMode: " + controller.getViewMode()); } } /** * Constructor * * @param controller The TimeLineController for this topcomponent. */ public TimeLineTopComponent(TimeLineController controller) { initComponents(); 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 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(AddBookmarkTagAction.BOOKMARK_SHORTCUT, "addBookmarkTag"); //NON-NLS getActionMap().put("addBookmarkTag", new AddBookmarkTagAction()); //NON-NLS this.controller = controller; //create linked result and content views contentViewerPanel = DataContentPanel.createInstance(); dataResultPanel = DataResultPanel.createInstanceUninitialized("", "", Node.EMPTY, 0, contentViewerPanel); //add them to bottom splitpane horizontalSplitPane.setLeftComponent(dataResultPanel); horizontalSplitPane.setRightComponent(contentViewerPanel); dataResultPanel.open(); //get the explorermanager Platform.runLater(this::initFXComponents); //set up listeners TimeLineController.getTimeZone().addListener(timeZone -> dataResultPanel.setPath(getResultViewerSummaryString())); controller.getSelectedEventIDs().addListener(selectedEventsListener); //Listen to ViewMode and adjust GUI componenets as needed. controller.viewModeProperty().addListener(viewMode -> syncViewMode()); syncViewMode(); } /** * Create and wire up JavaFX components of the interface */ @NbBundle.Messages({ "TimeLineTopComponent.eventsTab.name=Events", "TimeLineTopComponent.filterTab.name=Filters"}) @ThreadConfined(type = ThreadConfined.ThreadType.JFX) void initFXComponents() { /////init componenets of left most column from top to bottom final TimeZonePanel timeZonePanel = new TimeZonePanel(); VBox.setVgrow(timeZonePanel, Priority.SOMETIMES); HistoryToolBar historyToolBar = new HistoryToolBar(controller); final ZoomSettingsPane zoomSettingsPane = new ZoomSettingsPane(controller); //set up filter tab 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 //set up events tab final EventsTree eventsTree = new EventsTree(controller); 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().isNotEqualTo(ViewMode.DETAIL)); final TabPane leftTabPane = new TabPane(filterTab, eventsTreeTab); VBox.setVgrow(leftTabPane, Priority.ALWAYS); controller.viewModeProperty().addListener(viewMode -> { if (controller.getViewMode().equals(ViewMode.DETAIL) == false) { //if view mode is not details, switch back to the filter tab leftTabPane.getSelectionModel().select(filterTab); } }); //assemble left column final VBox leftVBox = new VBox(5, timeZonePanel, historyToolBar, zoomSettingsPane, leftTabPane); SplitPane.setResizableWithParent(leftVBox, Boolean.FALSE); final ViewFrame viewFrame = new ViewFrame(controller, eventsTree); final SplitPane mainSplitPane = new SplitPane(leftVBox, viewFrame); mainSplitPane.setDividerPositions(0); final Scene scene = new Scene(mainSplitPane); scene.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> { if (new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN).match(keyEvent)) { new Back(controller).handle(null); } else if (new KeyCodeCombination(KeyCode.BACK_SPACE).match(keyEvent)) { new Back(controller).handle(null); } else if (new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN).match(keyEvent)) { new Forward(controller).handle(null); } else if (new KeyCodeCombination(KeyCode.BACK_SPACE, KeyCodeCombination.SHIFT_DOWN).match(keyEvent)) { new Forward(controller).handle(null); } }); //add ui componenets to JFXPanels jFXViewPanel.setScene(scene); jFXstatusPanel.setScene(new Scene(new StatusBar(controller))); } @Override public List<Mode> availableModes(List<Mode> modes) { return Collections.emptyList(); } /** * 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 * regenerated by the Form Editor. */ // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { jFXstatusPanel = new javafx.embed.swing.JFXPanel(); splitYPane = new javax.swing.JSplitPane(); jFXViewPanel = new javafx.embed.swing.JFXPanel(); horizontalSplitPane = new javax.swing.JSplitPane(); leftFillerPanel = new javax.swing.JPanel(); rightfillerPanel = new javax.swing.JPanel(); jFXstatusPanel.setPreferredSize(new java.awt.Dimension(100, 16)); splitYPane.setDividerLocation(420); splitYPane.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT); splitYPane.setResizeWeight(0.9); splitYPane.setPreferredSize(new java.awt.Dimension(1024, 400)); splitYPane.setLeftComponent(jFXViewPanel); horizontalSplitPane.setDividerLocation(600); horizontalSplitPane.setResizeWeight(0.5); horizontalSplitPane.setPreferredSize(new java.awt.Dimension(1200, 300)); horizontalSplitPane.setRequestFocusEnabled(false); javax.swing.GroupLayout leftFillerPanelLayout = new javax.swing.GroupLayout(leftFillerPanel); leftFillerPanel.setLayout(leftFillerPanelLayout); leftFillerPanelLayout.setHorizontalGroup( leftFillerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGap(0, 599, Short.MAX_VALUE) ); leftFillerPanelLayout.setVerticalGroup( leftFillerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGap(0, 54, Short.MAX_VALUE) ); horizontalSplitPane.setLeftComponent(leftFillerPanel); javax.swing.GroupLayout rightfillerPanelLayout = new javax.swing.GroupLayout(rightfillerPanel); rightfillerPanel.setLayout(rightfillerPanelLayout); rightfillerPanelLayout.setHorizontalGroup( rightfillerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGap(0, 364, Short.MAX_VALUE) ); rightfillerPanelLayout.setVerticalGroup( rightfillerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGap(0, 54, Short.MAX_VALUE) ); horizontalSplitPane.setRightComponent(rightfillerPanel); splitYPane.setRightComponent(horizontalSplitPane); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(splitYPane, javax.swing.GroupLayout.DEFAULT_SIZE, 972, Short.MAX_VALUE) .addComponent(jFXstatusPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addComponent(splitYPane, javax.swing.GroupLayout.DEFAULT_SIZE, 482, Short.MAX_VALUE) .addGap(0, 0, 0) .addComponent(jFXstatusPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE)) ); }// </editor-fold>//GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JSplitPane horizontalSplitPane; private javafx.embed.swing.JFXPanel jFXViewPanel; private javafx.embed.swing.JFXPanel jFXstatusPanel; private javax.swing.JPanel leftFillerPanel; private javax.swing.JPanel rightfillerPanel; private javax.swing.JSplitPane splitYPane; // End of variables declaration//GEN-END:variables @Override public void componentOpened() { WindowManager.getDefault().setTopComponentFloating(this, true); putClientProperty(PROP_UNDOCKING_DISABLED, true); } @Override public ExplorerManager getExplorerManager() { return em; } /** * Get the string that should be used as the label above the result table. * It displays the time range spanned by the selected events. * * @return A String representation of all the events displayed. */ @NbBundle.Messages({ "# {0} - start of date range", "# {1} - end of date range", "TimeLineResultView.startDateToEndDate.text={0} to {1}"}) private String getResultViewerSummaryString() { Interval selectedTimeRange = controller.getSelectedTimeRange(); if (selectedTimeRange == null) { return ""; } else { final DateTimeFormatter zonedFormatter = TimeLineController.getZonedFormatter(); String start = selectedTimeRange.getStart() .withZone(TimeLineController.getJodaTimeZone()) .toString(zonedFormatter); String end = selectedTimeRange.getEnd() .withZone(TimeLineController.getJodaTimeZone()) .toString(zonedFormatter); return Bundle.TimeLineResultView_startDateToEndDate_text(start, end); } } }