/* * Autopsy Forensic Browser * * Copyright 2014-16 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.ui.detailview; import java.util.Arrays; import java.util.MissingResourceException; import java.util.function.Predicate; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.ObservableSet; import javafx.collections.SetChangeListener; import javafx.geometry.Side; import javafx.scene.chart.Axis; import javafx.scene.control.ContextMenu; import javafx.scene.control.Control; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import org.controlsfx.control.MasterDetailPane; 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.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; /** * A TimeLineChart that implements the visual aspects of the DetailView */ final class DetailsChart extends Control implements TimeLineChart<DateTime> { ///chart axes private final DateAxis detailsChartDateAxis; private final DateAxis pinnedDateAxis; private final Axis<EventStripe> verticalAxis; /** * Property that holds the interval selector if one is active */ private final SimpleObjectProperty<IntervalSelector<? extends DateTime>> intervalSelectorProp = new SimpleObjectProperty<>(); /** * ObservableSet of GuieLines displayed in this chart */ private final ObservableSet<GuideLine> guideLines = FXCollections.observableSet(); /** * Predicate used to determine if a EventNode should be highlighted. Can be * a combination of conditions such as: be in the selectedNodes list OR have * a particular description, but it must include be in the selectedNodes * (selectedNodes::contains). */ private final SimpleObjectProperty<Predicate<EventNodeBase<?>>> highlightPredicate = new SimpleObjectProperty<>((x) -> false); /** * An ObservableList of the Nodes that are selected in this chart. */ private final ObservableList<EventNodeBase<?>> selectedNodes; /** * An ObservableList representing all the events in the tree as a flat list * of events whose roots are in the eventStripes lists * */ private final ObservableList<TimeLineEvent> nestedEvents = FXCollections.observableArrayList(); /** * Aggregates all the settings related to the layout of this chart as one * object. */ private final DetailsChartLayoutSettings layoutSettings; /** * The main controller object for this instance of the Timeline UI. */ private final TimeLineController controller; /** * An ObservableList of root event stripes to display in the chart. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private final ObservableList<EventStripe> rootEventStripes = FXCollections.observableArrayList(); /** * Constructor * * @param controller The TimeLineController for this chart. * @param detailsChartDateAxis The DateAxis to use in this chart. * @param pinnedDateAxis The DateAxis to use for the pinned lane. It * will not be shown on screen, but must not be * null or the same as the detailsChartDateAxis. * @param verticalAxis An Axis<EventStripe> to use as the vertical * axis in the primary lane. * @param selectedNodes An ObservableList<EventNodeBase<?>>, that * will be used to keep track of the nodes * selected in this chart. */ DetailsChart(TimeLineController controller, DateAxis detailsChartDateAxis, DateAxis pinnedDateAxis, Axis<EventStripe> verticalAxis, ObservableList<EventNodeBase<?>> selectedNodes) { this.controller = controller; this.layoutSettings = new DetailsChartLayoutSettings(controller); this.detailsChartDateAxis = detailsChartDateAxis; this.verticalAxis = verticalAxis; this.pinnedDateAxis = pinnedDateAxis; this.selectedNodes = selectedNodes; FilteredEventsModel eventsModel = getController().getEventsModel(); /* * If the time range is changed, clear the guide line and the interval * selector, since they may not be in view any more. */ eventsModel.timeRangeProperty().addListener(o -> clearTimeBasedUIElements()); //if the view paramaters change, clear the selection eventsModel.zoomParametersProperty().addListener(o -> getSelectedNodes().clear()); } /** * Get the DateTime represented by the given x-position in this chart. * * * @param xPos The x-position to get the DataTime for. * * @return The DateTime represented by the given x-position in this chart. */ DateTime getDateTimeForPosition(double xPos) { return getXAxis().getValueForDisplay(getXAxis().parentToLocal(xPos, 0).getX()); } /** * Add an EventStripe to the list of root stripes. * * @param stripe The EventStripe to add. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) void addStripe(EventStripe stripe) { rootEventStripes.add(stripe); nestedEvents.add(stripe); } /** * Remove the given GuideLine from this chart. * * @param guideLine The GuideLine to remove. */ void clearGuideLine(GuideLine guideLine) { guideLines.remove(guideLine); } @Override public ObservableList<EventNodeBase<?>> getSelectedNodes() { return selectedNodes; } /** * Get the DetailsChartLayoutSettings for this chart. * * @return The DetailsChartLayoutSettings for this chart. */ DetailsChartLayoutSettings getLayoutSettings() { return layoutSettings; } /** * Set the Predicate used to determine if a EventNode should be highlighted. * Can be a combination of conditions such as: be in the selectedNodes list * OR have a particular description, but it must include be in the * selectedNodes (selectedNodes::contains). * * @param highlightPredicate The Predicate used to determine which nodes to * highlight. */ void setHighlightPredicate(Predicate<EventNodeBase<?>> highlightPredicate) { this.highlightPredicate.set(highlightPredicate); } /** * Remove all the events from this chart. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) void reset() { rootEventStripes.clear(); nestedEvents.clear(); } /** * Get the tree of event stripes flattened into a list */ public ObservableList<TimeLineEvent> getAllNestedEvents() { return nestedEvents; } /** * Clear any time based UI elements (GuideLines, IntervalSelector,...) from * this chart. */ private void clearTimeBasedUIElements() { guideLines.clear(); clearIntervalSelector(); } @Override public void clearIntervalSelector() { intervalSelectorProp.set(null); } @Override public IntervalSelector<DateTime> newIntervalSelector() { return new DetailIntervalSelector(this); } @Override public IntervalSelector<? extends DateTime> getIntervalSelector() { return intervalSelectorProp.get(); } @Override public void setIntervalSelector(IntervalSelector<? extends DateTime> newIntervalSelector) { intervalSelectorProp.set(newIntervalSelector); } @Override public Axis<DateTime> getXAxis() { return detailsChartDateAxis; } @Override public TimeLineController getController() { return controller; } @Override public void clearContextMenu() { setContextMenu(null); } @Override public ContextMenu getContextMenu(MouseEvent mouseEvent) throws MissingResourceException { //get the current context menu and hide it if it is not null ContextMenu contextMenu = getContextMenu(); if (contextMenu != null) { contextMenu.hide(); } //make and assign a new context menu based on the given mouseEvent setContextMenu(ActionUtils.createContextMenu(Arrays.asList( new PlaceMarkerAction(this, mouseEvent), ActionUtils.ACTION_SEPARATOR, TimeLineChart.newZoomHistoyActionGroup(getController()) ))); return getContextMenu(); } @Override protected Skin<?> createDefaultSkin() { return new DetailsChartSkin(this); } /** * Get the ObservableList of root EventStripes. * * @return The ObservableList of root EventStripes. */ ObservableList<EventStripe> getRootEventStripes() { return rootEventStripes; } /** * Implementation of IntervalSelector for use with a DetailsChart */ private static class DetailIntervalSelector extends IntervalSelector<DateTime> { /** * Constructor * * @param chart the chart this IntervalSelector belongs to. */ DetailIntervalSelector(IntervalSelector.IntervalSelectorProvider<DateTime> chart) { super(chart); } @Override protected String formatSpan(DateTime date) { return date.toString(TimeLineController.getZonedFormatter()); } @Override protected Interval adjustInterval(Interval i) { return i; } @Override protected DateTime parseDateTime(DateTime date) { return date; } } /** * Action that places a GuideLine at the location clicked by the user. */ static private class PlaceMarkerAction extends Action { private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); //NON-NLS private GuideLine guideLine; @NbBundle.Messages({"PlaceMArkerAction.name=Place Marker"}) PlaceMarkerAction(DetailsChart chart, MouseEvent clickEvent) { super(Bundle.PlaceMArkerAction_name()); setGraphic(new ImageView(MARKER)); // NON-NLS setEventHandler(actionEvent -> { if (guideLine == null) { guideLine = new GuideLine(chart); guideLine.relocate(chart.sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); chart.guideLines.add(guideLine); } else { guideLine.relocate(chart.sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); } }); } } /** * The Skin for DetailsChart that implements the visual display of the * chart. */ static private class DetailsChartSkin extends SkinBase<DetailsChart> { /** * If the pinned lane is visible this is the minimum height. */ private static final int MIN_PINNED_LANE_HEIGHT = 50; /* * The ChartLane for the main area of this chart. It is affected by all * the view settings. */ private final PrimaryDetailsChartLane primaryLane; /** * Container for the primary Lane that adds a vertical ScrollBar */ private final ScrollingLaneWrapper primaryView; /* * The ChartLane for the area of this chart that shows pinned eventsd. * It is not affected any filters. */ private final PinnedEventsChartLane pinnedLane; /** * Container for the pinned Lane that adds a vertical ScrollBar */ private final ScrollingLaneWrapper pinnedView; /** * Shows the two lanes with the primary lane as the master, and the * pinned lane as the details view above the primary lane. Used to show * and hide the pinned lane with a slide in/out animation. */ private final MasterDetailPane masterDetailPane; /** * Root Pane of this skin, */ private final Pane rootPane; /** * The divider position of masterDetailPane is saved when the pinned * lane is hidden so it can be restored when the pinned lane is shown * again. */ private double dividerPosition = .1; @NbBundle.Messages("DetailViewPane.pinnedLaneLabel.text=Pinned Events") DetailsChartSkin(DetailsChart chart) { super(chart); //initialize chart lanes; primaryLane = new PrimaryDetailsChartLane(chart, getSkinnable().detailsChartDateAxis, getSkinnable().verticalAxis); primaryView = new ScrollingLaneWrapper(primaryLane); pinnedLane = new PinnedEventsChartLane(chart, getSkinnable().pinnedDateAxis, new EventAxis<>(Bundle.DetailViewPane_pinnedLaneLabel_text())); pinnedView = new ScrollingLaneWrapper(pinnedLane); pinnedLane.setMinHeight(MIN_PINNED_LANE_HEIGHT); pinnedLane.maxVScrollProperty().addListener(maxVScroll -> syncPinnedHeight()); //assemble scene graph masterDetailPane = new MasterDetailPane(Side.TOP, primaryView, pinnedView, false); masterDetailPane.setDividerPosition(dividerPosition); masterDetailPane.prefHeightProperty().bind(getSkinnable().heightProperty()); masterDetailPane.prefWidthProperty().bind(getSkinnable().widthProperty()); rootPane = new Pane(masterDetailPane); getChildren().add(rootPane); //maintain highlighted effect on correct nodes getSkinnable().highlightPredicate.addListener((observable, oldPredicate, newPredicate) -> { primaryLane.getAllNodes().forEach(primaryNode -> primaryNode.applyHighlightEffect(newPredicate.test(primaryNode))); pinnedLane.getAllNodes().forEach(pinnedNode -> pinnedNode.applyHighlightEffect(newPredicate.test(pinnedNode))); }); //configure mouse listeners TimeLineChart.MouseClickedHandler<DateTime, DetailsChart> mouseClickedHandler = new TimeLineChart.MouseClickedHandler<>(getSkinnable()); TimeLineChart.ChartDragHandler<DateTime, DetailsChart> chartDragHandler = new TimeLineChart.ChartDragHandler<>(getSkinnable()); configureMouseListeners(primaryLane, mouseClickedHandler, chartDragHandler); configureMouseListeners(pinnedLane, mouseClickedHandler, chartDragHandler); //show and hide pinned lane in response to settings property change getSkinnable().getLayoutSettings().pinnedLaneShowing().addListener(observable -> syncPinnedLaneShowing()); syncPinnedLaneShowing(); //show and remove interval selector in sync with control state change getSkinnable().intervalSelectorProp.addListener((observable, oldIntervalSelector, newIntervalSelector) -> { rootPane.getChildren().remove(oldIntervalSelector); if (null != newIntervalSelector) { rootPane.getChildren().add(newIntervalSelector); } }); //show and remove guidelines in sync with control state change getSkinnable().guideLines.addListener((SetChangeListener.Change<? extends GuideLine> change) -> { if (change.wasRemoved()) { rootPane.getChildren().remove(change.getElementRemoved()); } if (change.wasAdded()) { rootPane.getChildren().add(change.getElementAdded()); } }); } /** * Sync the allowed height of the pinned lane's scroll pane to the lanes * actual height. */ private void syncPinnedHeight() { pinnedView.setMinHeight(MIN_PINNED_LANE_HEIGHT); pinnedView.setMaxHeight(pinnedLane.maxVScrollProperty().get() + 30); } /** * Add the given listeners to the given chart lane * * @param chartLane The Chart lane to add the listeners to. * @param mouseClickedHandler The MouseClickedHandler to add to chart. * @param chartDragHandler The ChartDragHandler to add to the chart * as pressed, released, dragged, and clicked * handler. */ static private void configureMouseListeners(final DetailsChartLane<?> chartLane, final TimeLineChart.MouseClickedHandler<DateTime, DetailsChart> mouseClickedHandler, final TimeLineChart.ChartDragHandler<DateTime, DetailsChart> chartDragHandler) { chartLane.setOnMousePressed(chartDragHandler); chartLane.setOnMouseReleased(chartDragHandler); chartLane.setOnMouseDragged(chartDragHandler); chartLane.setOnMouseClicked(chartDragHandler); chartLane.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedHandler); } /** * Show the pinned lane if and only if the settings object says it * should be. */ private void syncPinnedLaneShowing() { boolean pinnedLaneShowing = getSkinnable().getLayoutSettings().isPinnedLaneShowing(); if (pinnedLaneShowing == false) { //Save the divider position for later. dividerPosition = masterDetailPane.getDividerPosition(); } masterDetailPane.setShowDetailNode(pinnedLaneShowing); if (pinnedLaneShowing) { syncPinnedHeight(); //Restore the devider position. masterDetailPane.setDividerPosition(dividerPosition); } } } }