From 38a6fd3a0ff748f7c1cffba1c5e155eef02a4c44 Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 2 Mar 2016 10:28:17 -0500 Subject: [PATCH] show pinned events automatically, add labels to pinned and all event swim lanes move more code into DetailViewPane to allow guideline and interval selector to span the pinned and primary chart lanes pull common code into DetailsChartLane base class pull more common code into DetailsChartLane base class pull even more common code into DetailsChartLane base class --- .../ui/AbstractVisualizationPane.java | 4 + .../timeline/ui/ContextMenuProvider.java | 40 + .../autopsy/timeline/ui/IntervalSelector.java | 4 +- .../autopsy/timeline/ui/TimeLineChart.java | 36 +- .../ui/countsview/EventCountsChart.java | 6 +- .../ui/detailview/DetailViewPane.java | 194 ++++- .../timeline/ui/detailview/DetailsChart.java | 85 --- .../ui/detailview/DetailsChartLane.java | 456 ++++++++++++ .../timeline/ui/detailview/EventAxis.java | 4 + .../ui/detailview/EventClusterNode.java | 20 +- .../ui/detailview/EventDetailsChart.java | 683 ------------------ .../timeline/ui/detailview/EventNodeBase.java | 140 ++-- .../ui/detailview/EventStripeNode.java | 29 +- .../timeline/ui/detailview/GuideLine.java | 6 +- .../ui/detailview/MultiEventNodeBase.java | 17 +- .../ui/detailview/PinnedEventsChart.java | 402 ++--------- .../ui/detailview/PrimaryDetailsChart.java | 163 +++++ ...Wrapper.java => ScrollingLaneWrapper.java} | 21 +- .../ui/detailview/SingleEventNode.java | 12 +- 19 files changed, 1043 insertions(+), 1279 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java rename Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/{ScrollingWrapper.java => ScrollingLaneWrapper.java} (79%) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index 8a3ad0c784..9c4bbfa1d7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -132,6 +132,10 @@ public abstract class AbstractVisualizationPane settingsNodes; + public TimeLineController getController() { + return controller; + } + /** * @return the list of nodes containing settings widgets to insert into this * visualization's header diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java new file mode 100644 index 0000000000..b25d6f1972 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java @@ -0,0 +1,40 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.ui; + +import javafx.scene.chart.Axis; +import javafx.scene.control.ContextMenu; +import javafx.scene.input.MouseEvent; +import org.sleuthkit.autopsy.timeline.TimeLineController; + +public interface ContextMenuProvider { + + public Axis getXAxis(); + + public TimeLineController getController(); + + ContextMenu getContextMenu(); + + ContextMenu getChartContextMenu(MouseEvent m); + + IntervalSelector getIntervalSelector(); + + void setIntervalSelector(IntervalSelector newIntervalSelector); + + /** + * derived classes should implement this so as to supply an appropriate + * subclass of {@link IntervalSelector} + * + * @return a new interval selector + */ + IntervalSelector newIntervalSelector(); + + /** + * clear any references to previous interval selectors , including removing + * the interval selector from the ui / scene-graph + */ + void clearIntervalSelector(); +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java index 04869f3e0b..c8d38ca6ec 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java @@ -61,7 +61,7 @@ public abstract class IntervalSelector extends BorderPane { /** * the Axis this is a selector over */ - public final TimeLineChart chart; + public final ContextMenuProvider chart; private Tooltip tooltip; /////////drag state @@ -89,7 +89,7 @@ public abstract class IntervalSelector extends BorderPane { @FXML private BorderPane bottomBorder; - public IntervalSelector(TimeLineChart chart) { + public IntervalSelector(ContextMenuProvider chart) { this.chart = chart; this.controller = chart.getController(); FXMLConstructor.construct(this, IntervalSelector.class, "IntervalSelector.fxml"); // NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java index 0f34db262f..05ff1911b3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java @@ -22,10 +22,9 @@ import javafx.event.EventHandler; import javafx.event.EventType; import javafx.scene.Cursor; import javafx.scene.chart.Axis; -import javafx.scene.chart.Chart; -import javafx.scene.control.ContextMenu; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Region; import org.controlsfx.control.action.ActionGroup; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.TimeLineController; @@ -37,11 +36,7 @@ import org.sleuthkit.autopsy.timeline.actions.Forward; * * @param the type of values along the horizontal axis */ -public interface TimeLineChart { - - - - +public interface TimeLineChart extends ContextMenuProvider { // void setController(TimeLineController controller); IntervalSelector getIntervalSelector(); @@ -66,11 +61,6 @@ public interface TimeLineChart { public TimeLineController getController(); - - ContextMenu getChartContextMenu(); - - ContextMenu getChartContextMenu(MouseEvent m); - /** * drag handler class used by {@link TimeLineChart}s to create * {@link IntervalSelector}s @@ -78,7 +68,7 @@ public interface TimeLineChart { * @param the type of values along the horizontal axis * @param the type of chart this is a drag handler for */ - static class ChartDragHandler> implements EventHandler { + public static class ChartDragHandler> implements EventHandler { private final Y chart; @@ -102,8 +92,7 @@ public interface TimeLineChart { chart.getIntervalSelector().prefHeightProperty().bind(chart.heightProperty()); startX = mouseEvent.getX(); chart.getIntervalSelector().relocate(startX, 0); - } else { - //resize/position existing selector + } else //resize/position existing selector if (mouseEvent.getX() > startX) { chart.getIntervalSelector().relocate(startX, 0); chart.getIntervalSelector().setPrefWidth(mouseEvent.getX() - startX); @@ -111,7 +100,6 @@ public interface TimeLineChart { chart.getIntervalSelector().relocate(mouseEvent.getX(), 0); chart.getIntervalSelector().setPrefWidth(startX - mouseEvent.getX()); } - } chart.getIntervalSelector().autosize(); } else if (mouseEventType == MouseEvent.MOUSE_RELEASED) { chart.setCursor(Cursor.DEFAULT); @@ -122,7 +110,7 @@ public interface TimeLineChart { } - static class MouseClickedHandler> implements EventHandler { + static class MouseClickedHandler> implements EventHandler { private final C chart; @@ -131,15 +119,15 @@ public interface TimeLineChart { } @Override - public void handle(MouseEvent clickEvent) { - if (chart.getChartContextMenu() != null) { - chart.getChartContextMenu().hide(); + public void handle(MouseEvent mouseEvent) { + if (chart.getContextMenu() != null) { + chart.getContextMenu().hide(); } - if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) { - chart.getChartContextMenu(clickEvent); + if (mouseEvent.getButton() == MouseButton.SECONDARY && mouseEvent.isStillSincePress()) { + chart.getChartContextMenu(mouseEvent); chart.setOnMouseMoved(this); - chart.getChartContextMenu().show(chart, clickEvent.getScreenX(), clickEvent.getScreenY()); - clickEvent.consume(); + chart.getContextMenu().show(chart, mouseEvent.getScreenX(), mouseEvent.getScreenY()); + mouseEvent.consume(); } } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java index 6e2cf15534..3d5980c12f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java @@ -65,10 +65,6 @@ final class EventCountsChart extends StackedBarChart implements private static final Effect SELECTED_NODE_EFFECT = new Lighting(); private ContextMenu chartContextMenu; - @Override - public ContextMenu getChartContextMenu() { - return chartContextMenu; - } private final TimeLineController controller; private final FilteredEventsModel filteredEvents; @@ -163,7 +159,7 @@ final class EventCountsChart extends StackedBarChart implements * * @return the context menu for this chart */ - ContextMenu getContextMenu() { + public ContextMenu getContextMenu() { return chartContextMenu; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 947e0362dd..31f1cc4ef1 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -19,7 +19,10 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.MissingResourceException; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.InvalidationListener; @@ -27,6 +30,7 @@ import javafx.beans.Observable; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import javafx.collections.SetChangeListener; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.geometry.Side; @@ -36,6 +40,7 @@ import javafx.scene.control.Alert; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; +import javafx.scene.control.ContextMenu; import javafx.scene.control.CustomMenuItem; import javafx.scene.control.Label; import javafx.scene.control.MenuButton; @@ -47,12 +52,16 @@ import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.TreeItem; import javafx.scene.effect.Effect; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.stage.Modality; 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; @@ -63,44 +72,93 @@ 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.AbstractVisualizationPane; -import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChart.HideDescriptionAction; -import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChart.UnhideDescriptionAction; +import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider; +import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; +import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChartLane.HideDescriptionAction; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChartLane.UnhideDescriptionAction; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** - * Controller class for a {@link EventDetailsChart} based implementation of a + * Controller class for a {@link PrimaryDetailsChart} based implementation of a * TimeLineView. * * This class listens to changes in the assigned {@link FilteredEventsModel} and - * updates the internal {@link EventDetailsChart} to reflect the currently + * updates the internal {@link PrimaryDetailsChart} to reflect the currently * requested events. * * Concurrency Policy: Access to the private members clusterChart, dateAxis, * EventTypeMap, and dataSets is all linked directly to the ClusterChart which * must only be manipulated on the JavaFx thread. */ -public class DetailViewPane extends AbstractVisualizationPane, EventDetailsChart> { +public class DetailViewPane extends AbstractVisualizationPane, PrimaryDetailsChart> implements ContextMenuProvider { private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName()); private final DateAxis detailsChartDateAxis = new DateAxis(); private final DateAxis pinnedDateAxis = new DateAxis(); - private final Axis verticalAxis = new EventAxis<>(); + private final Axis verticalAxis = new EventAxis<>("All Events"); private MultipleSelectionModel> treeSelectionModel; private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - private final ScrollingWrapper mainView; - private final ScrollingWrapper pinnedView; + private final ScrollingLaneWrapper mainView; + private final ScrollingLaneWrapper pinnedView; private final DetailViewLayoutSettings layoutSettings; private final PinnedEventsChart pinnedChart; private final MasterDetailPane masterDetailPane; private double dividerPosition = .1; private static final int MIN_PINNED_LANE_HEIGHT = 50; + private ContextMenu contextMenu; + private IntervalSelector intervalSelector; + private final Pane rootPane; public ObservableList getEventStripes() { return chart.getEventStripes(); } + private static class DetailIntervalSelector extends IntervalSelector { + + DetailIntervalSelector(ContextMenuProvider 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; + } + } + + @Override + public ContextMenu getContextMenu() { + return contextMenu; + } + + @Override + public ContextMenu getChartContextMenu(MouseEvent mouseEvent) throws MissingResourceException { + if (contextMenu != null) { + contextMenu.hide(); + } + + contextMenu = ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(mouseEvent), + TimeLineChart.newZoomHistoyActionGroup(controller))); + contextMenu.setAutoHide(true); + return contextMenu; + } + + DetailViewLayoutSettings getLayoutSettings() { + return layoutSettings; + } + @Override protected void resetData() { for (XYChart.Series s : dataSeries) { @@ -117,19 +175,22 @@ public class DetailViewPane extends AbstractVisualizationPane(chart); - pinnedChart = new PinnedEventsChart(controller, pinnedDateAxis, new EventAxis<>(), selectedNodes, layoutSettings); - pinnedView = new ScrollingWrapper<>(pinnedChart); + chart = new PrimaryDetailsChart(this, detailsChartDateAxis, verticalAxis); + chart.setData(dataSeries); + setChartClickHandler(); //can we push this into chart + + mainView = new ScrollingLaneWrapper(chart); + pinnedChart = new PinnedEventsChart(this, pinnedDateAxis, new EventAxis<>("Pinned Events")); + pinnedView = new ScrollingLaneWrapper(pinnedChart); pinnedChart.setMinHeight(MIN_PINNED_LANE_HEIGHT); pinnedView.setMinHeight(MIN_PINNED_LANE_HEIGHT); - setChartClickHandler(); //can we push this into chart + masterDetailPane = new MasterDetailPane(Side.TOP, mainView, pinnedView, false); masterDetailPane.setDividerPosition(dividerPosition); -// SplitPane.setResizableWithParent(pinnedChart, Boolean.FALSE); -// SplitPane.setResizableWithParent(pinnedView, Boolean.FALSE); - chart.setData(dataSeries); - setCenter(masterDetailPane); + masterDetailPane.prefHeightProperty().bind(heightProperty()); + masterDetailPane.prefWidthProperty().bind(widthProperty()); + rootPane = new Pane(masterDetailPane); + setCenter(rootPane); settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable()); //bind layout fo axes and spacers @@ -158,6 +219,40 @@ public class DetailViewPane extends AbstractVisualizationPane detailNode.getEventIDs().stream()) .collect(Collectors.toList())); }); + + filteredEvents.zoomParametersProperty().addListener(o -> { + clearIntervalSelector(); + selectedNodes.clear(); + controller.selectEventIDs(Collections.emptyList()); + }); + + TimeLineChart.MouseClickedHandler mouseClickedHandler = new TimeLineChart.MouseClickedHandler<>(this); + TimeLineChart.ChartDragHandler chartDragHandler = new TimeLineChart.ChartDragHandler<>(this); + configureMouseListeners(chart, mouseClickedHandler, chartDragHandler); + configureMouseListeners(pinnedChart, mouseClickedHandler, chartDragHandler); + + } + + @Override + public void clearIntervalSelector() { + rootPane.getChildren().remove(intervalSelector); + intervalSelector = null; + } + + @Override + public IntervalSelector newIntervalSelector() { + return new DetailIntervalSelector(this); + } + + @Override + public IntervalSelector getIntervalSelector() { + return intervalSelector; + } + + @Override + public void setIntervalSelector(IntervalSelector newIntervalSelector) { + intervalSelector = newIntervalSelector; + rootPane.getChildren().add(getIntervalSelector()); } public void setSelectionModel(MultipleSelectionModel> selectionModel) { @@ -188,7 +283,7 @@ public class DetailViewPane extends AbstractVisualizationPane getXAxis() { + public Axis getXAxis() { return detailsChartDateAxis; } @@ -215,6 +310,57 @@ public class DetailViewPane extends AbstractVisualizationPane c1, Boolean selected) { c1.applySelectionEffect(selected); + + } + + DateTime getDateTimeForPosition(double layoutX) { + return chart.getDateTimeForPosition(layoutX); + } + + void clearGuideLine(GuideLine guideLine) { + rootPane.getChildren().remove(guideLine); + guideLine = null; + } + + /** + * + * @param chartLane the value of chartLane + * @param mouseClickedHandler the value of mouseClickedHandler + * @param chartDragHandler1 the value of chartDragHandler1 + */ + private void configureMouseListeners(final DetailsChartLane chartLane, final TimeLineChart.MouseClickedHandler mouseClickedHandler, final TimeLineChart.ChartDragHandler chartDragHandler) { + chartLane.setOnMousePressed(chartDragHandler); + chartLane.setOnMouseReleased(chartDragHandler); + chartLane.setOnMouseDragged(chartDragHandler); + chartLane.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedHandler); + } + private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); //NON-NLS + + class PlaceMarkerAction extends Action { + + private GuideLine guideLine; + + @NbBundle.Messages({"EventDetailChart.chartContextMenu.placeMarker.name=Place Marker"}) + PlaceMarkerAction(MouseEvent clickEvent) { + super(Bundle.EventDetailChart_chartContextMenu_placeMarker_name()); + + setGraphic(new ImageView(MARKER)); // NON-NLS + setEventHandler(actionEvent -> { + if (guideLine == null) { + guideLine = new GuideLine(DetailViewPane.this); + guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); + addGuideLine(guideLine); + + } else { + guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); + } + }); + } + + } + + private void addGuideLine(GuideLine guideLine) { + rootPane.getChildren().add(guideLine); } private class DetailViewSettingsPane extends HBox { @@ -331,7 +477,7 @@ public class DetailViewPane extends AbstractVisualizationPane { + pinnedEventsToggle.selectedProperty().addListener(observable -> { boolean selected = pinnedEventsToggle.isSelected(); if (selected == false) { dividerPosition = masterDetailPane.getDividerPosition(); @@ -344,7 +490,16 @@ public class DetailViewPane extends AbstractVisualizationPane change) -> { + if (change.getSet().size() == 0) { + pinnedEventsToggle.setSelected(false); + } else { + pinnedEventsToggle.setSelected(true); + } + }); } + } public Action newUnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) { @@ -353,6 +508,7 @@ public class DetailViewPane extends AbstractVisualizationPane { - - static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS - static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS - - public void requestTimelineChartLayout(); - - public Node asNode(); - - public ObservableList getEventStripes(); - - public ObservableList> getSelectedNodes(); - - double layoutEventBundleNodes(final Collection> nodes, final double minY); - - ReadOnlyDoubleProperty maxVScrollProperty(); - - void setVScroll(double vScrollValue); - - @NbBundle.Messages({"HideDescriptionAction.displayName=Hide", - "HideDescriptionAction.displayMsg=Hide this group from the details view."}) - static class HideDescriptionAction extends Action { - - HideDescriptionAction(String description, DescriptionLoD descriptionLoD, DetailsChart chart) { - super(Bundle.HideDescriptionAction_displayName()); - setLongText(Bundle.HideDescriptionAction_displayMsg()); - setGraphic(new ImageView(HIDE)); - setEventHandler((ActionEvent t) -> { - final DescriptionFilter testFilter = new DescriptionFilter( - descriptionLoD, - description, - DescriptionFilter.FilterMode.EXCLUDE); - - DescriptionFilter descriptionFilter = chart.getController().getQuickHideFilters().stream() - .filter(testFilter::equals) - .findFirst().orElseGet(() -> { - testFilter.selectedProperty().addListener(observable -> chart.requestTimelineChartLayout()); - chart.getController().getQuickHideFilters().add(testFilter); - return testFilter; - }); - descriptionFilter.setSelected(true); - }); - } - } - - @NbBundle.Messages({"UnhideDescriptionAction.displayName=Unhide"}) - static class UnhideDescriptionAction extends Action { - - UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD, DetailsChart chart) { - super(Bundle.UnhideDescriptionAction_displayName()); - setGraphic(new ImageView(SHOW)); - setEventHandler((ActionEvent t) -> - chart.getController().getQuickHideFilters().stream() - .filter(descriptionFilter -> descriptionFilter.getDescriptionLoD().equals(descriptionLoD) - && descriptionFilter.getDescription().equals(description)) - .forEach(descriptionfilter -> descriptionfilter.setSelected(false)) - ); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java new file mode 100644 index 0000000000..2afddc0f77 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java @@ -0,0 +1,456 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import com.google.common.collect.Range; +import com.google.common.collect.TreeRangeMap; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.chart.Axis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import static javafx.scene.layout.Region.USE_PREF_SIZE; +import org.controlsfx.control.action.Action; +import org.joda.time.DateTime; +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.filters.AbstractFilter; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; +import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; +import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; +import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; + +/** + * + * NOTE: It was too hard to control the threading of this chart via the + * complicated default listeners. Instead clients should use null {@link #addDataItem(javafx.scene.chart.XYChart.Data) + * } and {@link #removeDataItem(javafx.scene.chart.XYChart.Data) } to add and + * remove data. + */ +abstract class DetailsChartLane extends XYChart implements TimeLineChart { + + private static final String STYLE_SHEET = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS + + static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS + static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS + + static final int MINIMUM_EVENT_NODE_GAP = 4; + static final int MINIMUM_ROW_HEIGHT = 24; + + private final DetailViewPane parentPane; + final TimeLineController controller; + final FilteredEventsModel filteredEvents; + final DetailViewLayoutSettings layoutSettings; + final ObservableList> selectedNodes; + + final Map> eventMap = new HashMap<>(); + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + final ObservableList events = FXCollections.observableArrayList(); + final ObservableList< EventNodeBase> nodes = FXCollections.observableArrayList(); + final ObservableList< EventNodeBase> sortedNodes = nodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)); + private final boolean useQuickHideFilters; + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass + private Set activeQuickHidefilters; + + public boolean quickHideFiltersEnabled() { + return useQuickHideFilters; + } + + @Override + protected void layoutPlotChildren() { + setCursor(Cursor.WAIT); + maxY.set(0); + if (useQuickHideFilters) { + //These don't change during a layout pass and are expensive to compute per node. So we do it once at the start + activeQuickHidefilters = getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .map(DescriptionFilter::getDescription) + .collect(Collectors.toSet()); + } + //This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start + descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE; + + if (layoutSettings.getBandByType()) { + sortedNodes.stream() + .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() + .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); + } else { + maxY.set(layoutEventBundleNodes(sortedNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); + } + doAdditionalLayout(); + setCursor(null); + } + + @Override + public TimeLineController getController() { + return controller; + } + + public ObservableList> getSelectedNodes() { + return selectedNodes; + } + /** + * listener that triggers chart layout pass + */ + final InvalidationListener layoutInvalidationListener = (Observable o) -> { + layoutPlotChildren(); + }; + + public ReadOnlyDoubleProperty maxVScrollProperty() { + return maxY.getReadOnlyProperty(); + } + /** + * the maximum y value used so far during the most recent layout pass + */ + final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); + + DetailsChartLane(DetailViewPane parentPane, Axis dateAxis, Axis verticalAxis, boolean useQuickHideFilters) { + super(dateAxis, verticalAxis); + this.parentPane = parentPane; + this.layoutSettings = parentPane.getLayoutSettings(); + this.controller = parentPane.getController(); + this.selectedNodes = parentPane.getSelectedNodes(); + this.filteredEvents = controller.getEventsModel(); + this.useQuickHideFilters = useQuickHideFilters; + + Tooltip.install(this, AbstractVisualizationPane.getDefaultTooltip()); + + dateAxis.setAutoRanging(false); + verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm + verticalAxis.setTickLabelsVisible(false); + verticalAxis.setTickMarkVisible(false); + setLegendVisible(false); + setPadding(Insets.EMPTY); + setAlternativeColumnFillVisible(true); + + sceneProperty().addListener(observable -> { + Scene scene = getScene(); + if (scene != null && scene.getStylesheets().contains(STYLE_SHEET) == false) { + scene.getStylesheets().add(STYLE_SHEET); + } + }); + +// //add listener for events that should trigger layout + layoutSettings.bandByTypeProperty().addListener(layoutInvalidationListener); + layoutSettings.oneEventPerRowProperty().addListener(layoutInvalidationListener); + layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); + layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); + layoutSettings.descrVisibilityProperty().addListener(layoutInvalidationListener); + controller.getQuickHideFilters().addListener(layoutInvalidationListener); + + //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly + getPlotChildren().add(nodeGroup); + } + + /** + * Layout the nodes in the given list, starting form the given minimum y + * coordinate via the following algorithm: + * + * We start with a list of nodes (each representing an event) sorted by span + * start time of the underlying event + * + * - initialize empty map (maxXatY) from y-ranges to max used x-value + * + * - for each node: + * + * -- size the node based on its children (use this algorithm recursively) + * + * -- get the event's start position from the dateaxis + * + * -- to position node: check if maxXatY is to the left of the left x coord: + * if maxXatY is less than the left x coord, good, put the current node + * here, mark right x coord as maxXatY, go to next node ; if maxXatY is + * greater than the left x coord, increment y position, do check again until + * maxXatY less than left x coord. + * + * @param nodes collection of nodes to layout, sorted by event + * start time + * @param minY the minimum y coordinate to position the nodes + * at. + * @param descriptionWidth the value of the maximum description width to set + * for each node. + * + * @return the maximum y coordinate used by any of the layed out nodes. + */ + public double layoutEventBundleNodes(final Collection> nodes, final double minY) { + // map from y-ranges to maximum x + TreeRangeMap maxXatY = TreeRangeMap.create(); + + // maximum y values occupied by any of the given nodes, updated as nodes are layed out. + double localMax = minY; + + //for each node do a recursive layout to size it and then position it in first available slot + for (EventNodeBase bundleNode : nodes) { + //is the node hiden by a quick hide filter? + + boolean quickHide = useQuickHideFilters && activeQuickHidefilters.contains(bundleNode.getDescription()); + if (quickHide) { + //hide it and skip layout + bundleNode.setVisible(false); + bundleNode.setManaged(false); + } else { + layoutBundleHelper(bundleNode); + //get computed height and width + double h = bundleNode.getBoundsInLocal().getHeight(); + double w = bundleNode.getBoundsInLocal().getWidth(); + //get left and right x coords from axis plus computed width + double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation(); + double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP; + + //initial test position + double yTop = (layoutSettings.getOneEventPerRow()) + ? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end + : computeYTop(minY, h, maxXatY, xLeft, xRight); + + localMax = Math.max(yTop + h, localMax); + + if ((xLeft != bundleNode.getLayoutX()) || (yTop != bundleNode.getLayoutY())) { + //animate node to new position + bundleNode.animateTo(xLeft, yTop); + } + } + } + return localMax; //return new max + } + + final public void requestTimelineChartLayout() { + requestChartLayout(); + } + + double getXForEpochMillis(Long millis) { + DateTime dateTime = new DateTime(millis); + return getXAxis().getDisplayPosition(dateTime); + } + + abstract public ObservableList getEventStripes(); + + @Override + public ContextMenu getContextMenu() { + return parentPane.getContextMenu(); + } + + @Override + protected void dataItemAdded(Series series, int itemIndex, Data item) { + } + + @Override + protected void dataItemRemoved(Data item, Series series) { + } + + @Override + protected void dataItemChanged(Data item) { + } + + @Override + protected void seriesAdded(Series series, int seriesIndex) { + } + + @Override + protected void seriesRemoved(Series series) { + } + + @Override + public IntervalSelector getIntervalSelector() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void setIntervalSelector(IntervalSelector newIntervalSelector) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public IntervalSelector newIntervalSelector() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void clearIntervalSelector() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + /** + * get the DateTime along the x-axis that corresponds to the given + * x-coordinate in the coordinate system of this {@link PrimaryDetailsChart} + * + * @param x a x-coordinate in the space of this {@link PrimaryDetailsChart} + * + * @return the DateTime along the x-axis corresponding to the given x value + * (in the space of this {@link PrimaryDetailsChart} + */ + public DateTime getDateTimeForPosition(double x) { + return getXAxis().getValueForDisplay(getXAxis().parentToLocal(x, 0).getX()); + } + + @Override + public ContextMenu getChartContextMenu(MouseEvent clickEvent) { + return parentPane.getChartContextMenu(clickEvent); + } + /** + * the group that all event nodes are added to. This facilitates scrolling + * by allowing a single translation of this group. + */ + final Group nodeGroup = new Group(); + + public synchronized void setVScroll(double vScrollValue) { + nodeGroup.setTranslateY(-vScrollValue); + } + + /** + * @return all the nodes that pass the given predicate + */ + synchronized Iterable> getNodes(Predicate> p) { + //use this recursive function to flatten the tree of nodes into an single stream. + Function, Stream>> stripeFlattener = + new Function, Stream>>() { + @Override + public Stream> apply(EventNodeBase node) { + return Stream.concat( + Stream.of(node), + node.getSubNodes().stream().flatMap(this::apply)); + } + }; + + return sortedNodes.stream() + .flatMap(stripeFlattener) + .filter(p).collect(Collectors.toList()); + } + @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass + double descriptionWidth; + + /** + * Given information about the current layout pass so far and about a + * particular node, compute the y position of that node. + * + * + * @param yMin the smallest (towards the top of the screen) y position to + * consider + * @param h the height of the node we are trying to position + * @param maxXatY a map from y ranges to the max x within that range. NOTE: + * This map will be updated to include the node in question. + * @param xLeft the left x-cord of the node to position + * @param xRight the left x-cord of the node to position + * + * @return the y position for the node in question. + * + * + */ + double computeYTop(double yMin, double h, TreeRangeMap maxXatY, double xLeft, double xRight) { + double yTop = yMin; + double yBottom = yTop + h; + //until the node is not overlapping any others try moving it down. + boolean overlapping = true; + while (overlapping) { + overlapping = false; + //check each pixel from bottom to top. + for (double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) { + final Double maxX = maxXatY.get(y); + if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) { + //if that pixel is already used + //jump top to this y value and repeat until free slot is found. + overlapping = true; + yTop = y + MINIMUM_EVENT_NODE_GAP; + yBottom = yTop + h; + break; + } + } + } + maxXatY.put(Range.closed(yTop, yBottom), xRight); + return yTop; + } + + /** + * + * Set layout paramaters on the given node and layout its children + * + * @param eventNode the Node to layout + * @param descriptionWdith the maximum width for the description text + */ + void layoutBundleHelper(final EventNodeBase< ?> eventNode) { + //make sure it is shown + eventNode.setVisible(true); + eventNode.setManaged(true); + //apply advanced layout description visibility options + eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility()); + eventNode.setMaxDescriptionWidth(descriptionWidth); + + //do recursive layout + eventNode.layoutChildren(); + } + + abstract void doAdditionalLayout(); + + @NbBundle.Messages({"HideDescriptionAction.displayName=Hide", + "HideDescriptionAction.displayMsg=Hide this group from the details view."}) + static class HideDescriptionAction extends Action { + + HideDescriptionAction(String description, DescriptionLoD descriptionLoD, DetailsChartLane chart) { + super(Bundle.HideDescriptionAction_displayName()); + setLongText(Bundle.HideDescriptionAction_displayMsg()); + setGraphic(new ImageView(HIDE)); + setEventHandler((ActionEvent t) -> { + final DescriptionFilter testFilter = new DescriptionFilter( + descriptionLoD, + description, + DescriptionFilter.FilterMode.EXCLUDE); + + DescriptionFilter descriptionFilter = chart.getController().getQuickHideFilters().stream() + .filter(testFilter::equals) + .findFirst().orElseGet(() -> { + testFilter.selectedProperty().addListener(observable -> chart.requestTimelineChartLayout()); + chart.getController().getQuickHideFilters().add(testFilter); + return testFilter; + }); + descriptionFilter.setSelected(true); + }); + } + } + + @NbBundle.Messages({"UnhideDescriptionAction.displayName=Unhide"}) + static class UnhideDescriptionAction extends Action { + + UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD, DetailsChartLane chart) { + super(Bundle.UnhideDescriptionAction_displayName()); + setGraphic(new ImageView(SHOW)); + setEventHandler((ActionEvent t) -> + chart.getController().getQuickHideFilters().stream() + .filter(descriptionFilter -> descriptionFilter.getDescriptionLoD().equals(descriptionLoD) + && descriptionFilter.getDescription().equals(description)) + .forEach(descriptionfilter -> descriptionfilter.setSelected(false)) + ); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java index d68f390894..40b2309d3a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java @@ -30,6 +30,10 @@ import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; */ class EventAxis extends Axis { + EventAxis(String label) { + setLabel(label); + } + @Override public double getDisplayPosition(Type value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index 5bb1e86c43..0be313272c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -89,8 +89,8 @@ final public class EventClusterNode extends MultiEventNodeBase chartLane, EventCluster eventCluster, EventStripeNode parentNode) { + super(chartLane, eventCluster, parentNode); subNodePane.setBorder(clusterBorder); subNodePane.setBackground(defaultBackground); @@ -146,7 +146,7 @@ final public class EventClusterNode extends MultiEventNodeBase createChildNode(EventStripe stripe) { if (stripe.getEventIDs().size() == 1) { - return new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(stripe.getEventIDs())), this); + return new SingleEventNode(getChartLane(), getChartLane().getController().getEventsModel().getEventById(Iterables.getOnlyElement(stripe.getEventIDs())), this); } else { - return new EventStripeNode(getChart(), stripe, this); + return new EventStripeNode(getChartLane(), stripe, this); } } @@ -231,8 +231,8 @@ final public class EventClusterNode extends MultiEventNodeBase 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.detailview; - -import com.google.common.collect.Iterables; -import com.google.common.collect.Range; -import com.google.common.collect.TreeRangeMap; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Map; -import java.util.MissingResourceException; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; -import javafx.beans.property.ReadOnlyDoubleProperty; -import javafx.beans.property.ReadOnlyDoubleWrapper; -import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.geometry.Insets; -import javafx.scene.Cursor; -import javafx.scene.Group; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.chart.Axis; -import javafx.scene.chart.NumberAxis; -import javafx.scene.chart.XYChart; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.Tooltip; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.input.MouseEvent; -import javafx.scene.shape.Line; -import javafx.scene.shape.StrokeLineCap; -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.EventCluster; -import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; -import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; -import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; -import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; -import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; - -/** - * Custom implementation of {@link XYChart} to graph events on a horizontal - * timeline. - * - * The horizontal {@link DateAxis} controls the tick-marks and the horizontal - * layout of the nodes representing events. The vertical {@link NumberAxis} does - * nothing (although a custom implementation could help with the vertical - * layout?) - * - * Series help organize events for the banding by event type, we could add a - * node to contain each band if we need a place for per band controls. - * - * NOTE: It was too hard to control the threading of this chart via the - * complicated default listeners. Instead clients should use null {@link #addDataItem(javafx.scene.chart.XYChart.Data) - * } and {@link #removeDataItem(javafx.scene.chart.XYChart.Data) } to add and - * remove data. - * - * //TODO: refactor the projected lines to a separate class. -jm - */ -public final class EventDetailsChart extends XYChart implements DetailsChart { - - private static final String styleSheet = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS - - private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); //NON-NLS - private static final int PROJECTED_LINE_Y_OFFSET = 5; - private static final int PROJECTED_LINE_STROKE_WIDTH = 5; - private static final int MINIMUM_EVENT_NODE_GAP = 4; - private final static int MINIMUM_ROW_HEIGHT = 24; - - private final TimeLineController controller; - private final FilteredEventsModel filteredEvents; - - private ContextMenu chartContextMenu; - - @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass - private Set activeQuickHidefilters; - @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass - private double descriptionWidth; - private final DetailViewLayoutSettings layoutSettings; - - @Override - public ContextMenu getChartContextMenu() { - return chartContextMenu; - } - - /** - * a user positionable vertical line to help compare events - */ - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - private Line guideLine; - - /** - * * the user can drag out a time range to zoom into and this - * {@link IntervalSelector} is the visual representation of it while the - * user is dragging - */ - private IntervalSelector intervalSelector; - - /** - * listener that triggers chart layout pass - */ - private final InvalidationListener layoutInvalidationListener = (Observable o) -> { - layoutPlotChildren(); - }; - - /** - * the maximum y value used so far during the most recent layout pass - */ - private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); - - final ObservableList> selectedNodes; - /** - * the group that all event nodes are added to. This facilitates scrolling - * by allowing a single translation of this group. - */ - private final Group nodeGroup = new Group(); - - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - private final ObservableList eventStripes = FXCollections.observableArrayList(); - private final ObservableList< EventNodeBase> stripeNodes = FXCollections.observableArrayList(); - private final ObservableList< EventNodeBase> sortedStripeNodes = stripeNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)); - private final Map projectionMap = new ConcurrentHashMap<>(); - - EventDetailsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes, DetailViewLayoutSettings layoutSettings) { - super(dateAxis, verticalAxis); - this.layoutSettings = layoutSettings; - - this.controller = controller; - this.filteredEvents = this.controller.getEventsModel(); - - sceneProperty().addListener(observable -> { - Scene scene = getScene(); - if (scene != null && scene.getStylesheets().contains(styleSheet) == false) { - scene.getStylesheets().add(styleSheet); - } - }); - - filteredEvents.zoomParametersProperty().addListener(o -> { - clearGuideLine(); - clearIntervalSelector(); - selectedNodes.clear(); - projectionMap.clear(); - controller.selectEventIDs(Collections.emptyList()); - }); - - Tooltip.install(this, AbstractVisualizationPane.getDefaultTooltip()); - - dateAxis.setAutoRanging(false); - verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm - verticalAxis.setTickLabelsVisible(false); - verticalAxis.setTickMarkVisible(false); - setLegendVisible(false); - setPadding(Insets.EMPTY); - setAlternativeColumnFillVisible(true); - - //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly - getPlotChildren().add(nodeGroup); - - //add listener for events that should trigger layout - layoutSettings.bandByTypeProperty().addListener(layoutInvalidationListener); - layoutSettings.oneEventPerRowProperty().addListener(layoutInvalidationListener); - layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); - layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); - layoutSettings.descrVisibilityProperty().addListener(layoutInvalidationListener); - getController().getQuickHideFilters().addListener(layoutInvalidationListener); - - //this is needed to allow non circular binding of the guideline and timerangeRect heights to the height of the chart - //TODO: seems like a hack, can we remove? -jm - boundsInLocalProperty().addListener((Observable observable) -> { - setPrefHeight(boundsInLocalProperty().get().getHeight()); - }); - - ChartDragHandler chartDragHandler = new ChartDragHandler<>(this); - setOnMousePressed(chartDragHandler); - setOnMouseReleased(chartDragHandler); - setOnMouseDragged(chartDragHandler); - - setOnMouseClicked(new MouseClickedHandler<>(this)); - - this.selectedNodes = selectedNodes; - this.selectedNodes.addListener(new SelectionChangeHandler()); - } - - @Override - public void requestTimelineChartLayout() { - requestChartLayout(); - } - - public ObservableList> getSelectedNodes() { - return selectedNodes; - } - - @Override - public Node asNode() { - return this; - } - - public ObservableList getEventStripes() { - return eventStripes; - } - - @Override - public TimeLineController getController() { - return controller; - } - - @Override - public ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException { - if (chartContextMenu != null) { - chartContextMenu.hide(); - } - - chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(clickEvent), - TimeLineChart.newZoomHistoyActionGroup(controller))); - chartContextMenu.setAutoHide(true); - return chartContextMenu; - } - - @Override - public void clearIntervalSelector() { - getChartChildren().remove(intervalSelector); - intervalSelector = null; - } - - @Override - public IntervalSelector newIntervalSelector() { - return new DetailIntervalSelector(this); - } - - /** - * get the DateTime along the x-axis that corresponds to the given - * x-coordinate in the coordinate system of this {@link EventDetailsChart} - * - * @param x a x-coordinate in the space of this {@link EventDetailsChart} - * - * @return the DateTime along the x-axis corresponding to the given x value - * (in the space of this {@link EventDetailsChart} - */ - public DateTime getDateTimeForPosition(double x) { - return getXAxis().getValueForDisplay(getXAxis().parentToLocal(x, 0).getX()); - } - - @Override - public IntervalSelector getIntervalSelector() { - return intervalSelector; - } - - @Override - public void setIntervalSelector(IntervalSelector newIntervalSelector) { - intervalSelector = newIntervalSelector; - getChartChildren().add(getIntervalSelector()); - } - - /** - * @see note in main section of class JavaDoc - * - * @param series - * @param i - */ - @Override - protected void seriesAdded(Series series, int i) { - - } - - /** - * @see note in main section of class JavaDoc - * - * @param series - */ - @Override - protected void seriesRemoved(Series series) { - - } - - /** - * @see note in main section of class JavaDoc - * - * @param series - * @param itemIndex - * @param item - */ - @Override - protected void dataItemAdded(Series series, int itemIndex, Data item) { - } - - /** - * @see note in main section of class JavaDoc - * - * - * @param item - * @param series - */ - @Override - protected void dataItemRemoved(Data item, Series series) { - } - - /** - * @see note in main section of class JavaDoc - * - * @param item - */ - @Override - protected void dataItemChanged(Data item) { - } - - /** - * add a dataitem to this chart - * - * @see note in main section of class JavaDoc - * - * @param data - */ - void addDataItem(Data data) { - final EventStripe eventStripe = data.getYValue(); - EventNodeBase newNode; - if (eventStripe.getEventIDs().size() == 1) { - newNode = new SingleEventNode(this, controller.getEventsModel().getEventById(Iterables.getOnlyElement(eventStripe.getEventIDs())), null); - } else { - newNode = new EventStripeNode(EventDetailsChart.this, eventStripe, null); - } - Platform.runLater(() -> { - eventStripes.add(eventStripe); - stripeNodes.add(newNode); - nodeGroup.getChildren().add(newNode); - data.setNode(newNode); - }); - } - - /** - * remove a data item from this chart - * - * @see note in main section of class JavaDoc - * - * @param data - */ - void removeDataItem(Data data) { - Platform.runLater(() -> { - EventNodeBase removedNode = (EventNodeBase) data.getNode(); - eventStripes.removeAll(new StripeFlattener().apply(removedNode).collect(Collectors.toList())); - stripeNodes.removeAll(removedNode); - nodeGroup.getChildren().removeAll(removedNode); - data.setNode(null); - }); - } - - @Override - protected void layoutPlotChildren() { - setCursor(Cursor.WAIT); - maxY.set(0); - - //These don't change during a layout pass and are expensive to compute per node. So we do it once at the start - activeQuickHidefilters = getController().getQuickHideFilters().stream() - .filter(AbstractFilter::isActive) - .map(DescriptionFilter::getDescription) - .collect(Collectors.toSet()); - - //This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start - descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE; - - if (layoutSettings.getBandByType()) { - sortedStripeNodes.stream() - .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() - .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); - } else { - maxY.set(layoutEventBundleNodes(sortedStripeNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); - } - layoutProjectionMap(); - setCursor(null); - } - - public ReadOnlyDoubleProperty maxVScrollProperty() { - return maxY.getReadOnlyProperty(); - } - - /** - * @return all the nodes that pass the given predicate - */ - synchronized Iterable> getNodes(Predicate> p) { - //use this recursive function to flatten the tree of nodes into an single stream. - Function, Stream>> stripeFlattener = - new Function, Stream>>() { - @Override - public Stream> apply(EventNodeBase node) { - return Stream.concat( - Stream.of(node), - node.getSubNodes().stream().flatMap(this::apply)); - } - }; - - return sortedStripeNodes.stream() - .flatMap(stripeFlattener) - .filter(p).collect(Collectors.toList()); - } - - public synchronized void setVScroll(double vScrollValue) { - nodeGroup.setTranslateY(-vScrollValue); - } - - void clearGuideLine() { - getChartChildren().remove(guideLine); - guideLine = null; - } - - /** - * Layout the nodes in the given list, starting form the given minimum y - * coordinate via the following algorithm: - * - * We start with a list of nodes (each representing an event) sorted by span - * start time of the underlying event - * - * - initialize empty map (maxXatY) from y-ranges to max used x-value - * - * - for each node: - * - * -- size the node based on its children (use this algorithm recursively) - * - * -- get the event's start position from the dateaxis - * - * -- to position node: check if maxXatY is to the left of the left x coord: - * if maxXatY is less than the left x coord, good, put the current node - * here, mark right x coord as maxXatY, go to next node ; if maxXatY is - * greater than the left x coord, increment y position, do check again until - * maxXatY less than left x coord. - * - * @param nodes collection of nodes to layout, sorted by event - * start time - * @param minY the minimum y coordinate to position the nodes - * at. - * @param descriptionWidth the value of the maximum description width to set - * for each node. - * - * @return the maximum y coordinate used by any of the layed out nodes. - */ - public double layoutEventBundleNodes(final Collection> nodes, final double minY) { - // map from y-ranges to maximum x - TreeRangeMap maxXatY = TreeRangeMap.create(); - - // maximum y values occupied by any of the given nodes, updated as nodes are layed out. - double localMax = minY; - - //for each node do a recursive layout to size it and then position it in first available slot - for (EventNodeBase bundleNode : nodes) { - //is the node hiden by a quick hide filter? - boolean quickHide = activeQuickHidefilters.contains(bundleNode.getDescription()); - if (quickHide) { - //hide it and skip layout - bundleNode.setVisible(false); - bundleNode.setManaged(false); - } else { - layoutBundleHelper(bundleNode); - //get computed height and width - double h = bundleNode.getBoundsInLocal().getHeight(); - double w = bundleNode.getBoundsInLocal().getWidth(); - //get left and right x coords from axis plus computed width - double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation(); - double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP; - - //initial test position - double yTop = (layoutSettings.getOneEventPerRow()) - ? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end - : computeYTop(minY, h, maxXatY, xLeft, xRight); - - localMax = Math.max(yTop + h, localMax); - - if ((xLeft != bundleNode.getLayoutX()) || (yTop != bundleNode.getLayoutY())) { - //animate node to new position - bundleNode.animateTo(xLeft, yTop); - } - } - } - return localMax; //return new max - } - - /** - * Given information about the current layout pass so far and about a - * particular node, compute the y position of that node. - * - * - * @param yMin the smallest (towards the top of the screen) y position to - * consider - * @param h the height of the node we are trying to position - * @param maxXatY a map from y ranges to the max x within that range. NOTE: - * This map will be updated to include the node in question. - * @param xLeft the left x-cord of the node to position - * @param xRight the left x-cord of the node to position - * - * @return the y position for the node in question. - * - * - */ - private double computeYTop(double yMin, double h, TreeRangeMap maxXatY, double xLeft, double xRight) { - double yTop = yMin; - double yBottom = yTop + h; - //until the node is not overlapping any others try moving it down. - boolean overlapping = true; - while (overlapping) { - overlapping = false; - //check each pixel from bottom to top. - for (double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) { - final Double maxX = maxXatY.get(y); - if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) { - //if that pixel is already used - //jump top to this y value and repeat until free slot is found. - overlapping = true; - yTop = y + MINIMUM_EVENT_NODE_GAP; - yBottom = yTop + h; - break; - } - } - } - maxXatY.put(Range.closed(yTop, yBottom), xRight); - return yTop; - } - - /** - * - * Set layout paramaters on the given node and layout its children - * - * @param eventNode the Node to layout - * @param descriptionWdith the maximum width for the description text - */ - private void layoutBundleHelper(final EventNodeBase< ?> eventNode) { - //make sure it is shown - eventNode.setVisible(true); - eventNode.setManaged(true); - //apply advanced layout description visibility options - eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility()); - eventNode.setMaxDescriptionWidth(descriptionWidth); - - //do recursive layout - eventNode.layoutChildren(); - } - - /** - * expose as protected - */ - @Override - protected void requestChartLayout() { - super.requestChartLayout(); - } - - private double getXForEpochMillis(Long millis) { - DateTime dateTime = new DateTime(millis); - return getXAxis().getDisplayPosition(dateTime); - } - - private double getParentXForEpochMillis(Long epochMillis) { - return getXAxis().localToParent(getXForEpochMillis(epochMillis), 0).getX(); - } - - private void layoutProjectionMap() { - for (final Map.Entry entry : projectionMap.entrySet()) { - final EventCluster cluster = entry.getKey(); - final Line line = entry.getValue(); - - line.setStartX(getParentXForEpochMillis(cluster.getStartMillis())); - line.setEndX(getParentXForEpochMillis(cluster.getEndMillis())); - - line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); - line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); - } - } - - /** - * @return the filteredEvents - */ - public FilteredEventsModel getFilteredEvents() { - return filteredEvents; - - } - - static private class DetailIntervalSelector extends IntervalSelector { - - DetailIntervalSelector(EventDetailsChart 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; - } - } - - private class PlaceMarkerAction extends Action { - - @NbBundle.Messages({"EventDetailChart.chartContextMenu.placeMarker.name=Place Marker"}) - PlaceMarkerAction(MouseEvent clickEvent) { - super(Bundle.EventDetailChart_chartContextMenu_placeMarker_name()); - - setGraphic(new ImageView(MARKER)); // NON-NLS - setEventHandler(actionEvent -> { - if (guideLine == null) { - guideLine = new GuideLine(EventDetailsChart.this); - guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); - getChartChildren().add(guideLine); - - } else { - guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); - } - }); - } - } - - private class SelectionChangeHandler implements ListChangeListener> { - - private final Axis dateAxis; - - SelectionChangeHandler() { - dateAxis = getXAxis(); - } - - @Override - public void onChanged(ListChangeListener.Change> change) { - while (change.next()) { - change.getRemoved().forEach((EventNodeBase removedNode) -> { - removedNode.getEvent().getClusters().forEach(cluster -> { - Line removedLine = projectionMap.remove(cluster); - getChartChildren().removeAll(removedLine); - }); - - }); - change.getAddedSubList().forEach((EventNodeBase addedNode) -> { - - for (EventCluster range : addedNode.getEvent().getClusters()) { - - Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, - dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET - ); - line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5)); - line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); - line.setStrokeLineCap(StrokeLineCap.ROUND); - projectionMap.put(range, line); - getChartChildren().add(line); - } - }); - } - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java index ef93dba265..6fda53d6df 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -71,7 +71,6 @@ import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; -import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show; import static org.sleuthkit.autopsy.timeline.ui.detailview.MultiEventNodeBase.CORNER_RADII_3; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; @@ -83,13 +82,14 @@ import org.sleuthkit.datamodel.TskCoreException; * */ public abstract class EventNodeBase extends StackPane { + static final Map dropShadowMap = new ConcurrentHashMap<>(); - + private static final Logger LOGGER = Logger.getLogger(EventNodeBase.class.getName()); - + private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N NON-NLS private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N - + private static final Image PIN = new Image("/org/sleuthkit/autopsy/timeline/images/marker--plus.png"); // NON-NLS //NOI18N private static final Image UNPIN = new Image("/org/sleuthkit/autopsy/timeline/images/marker--minus.png"); // NON-NLS //NOI18N @@ -98,59 +98,59 @@ public abstract class EventNodeBase extends StackPan b.setMaxSize(16, 16); b.setPrefSize(16, 16); } - + static void show(Node b, boolean show) { b.setVisible(show); b.setManaged(show); } - + final Type tlEvent; - + final EventNodeBase parentNode; - + final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); final SimpleObjectProperty descVisibility = new SimpleObjectProperty<>(); - - final DetailsChart chart; + + final DetailsChartLane chartLane; final Background highlightedBackground; final Background defaultBackground; final Color evtColor; - + final Label countLabel = new Label(); final Label descrLabel = new Label(); final ImageView hashIV = new ImageView(HASH_PIN); final ImageView tagIV = new ImageView(TAG); - + final HBox controlsHBox = new HBox(5); final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV, controlsHBox); - + final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); - + final ImageView eventTypeImageView = new ImageView(); final SleuthkitCase sleuthkitCase; final FilteredEventsModel eventsModel; private Timeline timeline; private Button pinButton; private final Border SELECTION_BORDER; - - EventNodeBase(Type ievent, EventNodeBase parent, DetailsChart chart) { - this.chart = chart; + + EventNodeBase(Type ievent, EventNodeBase parent, DetailsChartLane chart) { + this.chartLane = chart; this.tlEvent = ievent; this.parentNode = parent; - + sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); eventsModel = chart.getController().getEventsModel(); eventTypeImageView.setImage(getEventType().getFXImage()); - + descrLabel.setGraphic(eventTypeImageView); - + if (chart.getController().getEventsModel().getEventTypeZoom() == EventTypeZoomLevel.SUB_TYPE) { evtColor = getEventType().getColor(); } else { evtColor = getEventType().getBaseType().getColor(); } SELECTION_BORDER = new Border(new BorderStroke(evtColor.darker().desaturate(), BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2))); - + defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY)); highlightedBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY)); descVisibility.addListener(observable -> setDescriptionVisibiltiyImpl(descVisibility.get())); @@ -159,7 +159,7 @@ public abstract class EventNodeBase extends StackPan //set up mouse hover effect and tooltip setOnMouseEntered(mouseEntered -> { - Tooltip.uninstall(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); + Tooltip.uninstall(chart, AbstractVisualizationPane.getDefaultTooltip()); showHoverControls(true); toFront(); }); @@ -168,18 +168,20 @@ public abstract class EventNodeBase extends StackPan if (parentNode != null) { parentNode.showHoverControls(true); } else { - Tooltip.install(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); + Tooltip.install(chart, AbstractVisualizationPane.getDefaultTooltip()); } }); setOnMouseClicked(new ClickHandler()); show(controlsHBox, false); } - + public Type getEvent() { return tlEvent; } - - public abstract TimeLineChart getChart(); + + DetailsChartLane getChartLane() { + return chartLane; + } /** * @param w the maximum width the description label should have @@ -187,7 +189,7 @@ public abstract class EventNodeBase extends StackPan public void setMaxDescriptionWidth(double w) { descrLabel.setMaxWidth(w); } - + public abstract List> getSubNodes(); /** @@ -198,7 +200,7 @@ public abstract class EventNodeBase extends StackPan public void applySelectionEffect(boolean applied) { setBorder(applied ? SELECTION_BORDER : null); } - + protected void layoutChildren() { super.layoutChildren(); } @@ -215,7 +217,7 @@ public abstract class EventNodeBase extends StackPan configureActionButton(pinButton); } } - + final void showHoverControls(final boolean showControls) { Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(), eventType -> new DropShadow(-10, eventType.getColor())); @@ -223,9 +225,9 @@ public abstract class EventNodeBase extends StackPan installTooltip(); enableTooltip(showControls); installActionButtons(); - - TimeLineController controller = getChart().getController(); - + + TimeLineController controller = getChartLane().getController(); + if (controller.getPinnedEvents().contains(tlEvent)) { pinButton.setOnAction(actionEvent -> { new UnPinEventAction(controller, tlEvent).handle(actionEvent); @@ -239,7 +241,7 @@ public abstract class EventNodeBase extends StackPan }); pinButton.setGraphic(new ImageView(PIN)); } - + show(controlsHBox, showControls); if (parentNode != null) { parentNode.showHoverControls(false); @@ -268,7 +270,7 @@ public abstract class EventNodeBase extends StackPan { updateTitle(Bundle.EventNodeBase_toolTip_loading2()); } - + @Override protected String call() throws Exception { HashMap hashSetCounts = new HashMap<>(); @@ -288,7 +290,7 @@ public abstract class EventNodeBase extends StackPan String hashSetCountsString = hashSetCounts.entrySet().stream() .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) .collect(Collectors.joining("\n")); - + Map tagCounts = new HashMap<>(); if (tlEvent.getEventIDsWithTags().isEmpty() == false) { tagCounts.putAll(eventsModel.getTagCountsByTagName(tlEvent.getEventIDsWithTags())); @@ -296,14 +298,14 @@ public abstract class EventNodeBase extends StackPan String tagCountsString = tagCounts.entrySet().stream() .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) .collect(Collectors.joining("\n")); - + return Bundle.EventNodeBase_tooltip_text(getEventIDs().size(), getEventType(), getDescription(), TimeLineController.getZonedFormatter().print(getStartMillis()), TimeLineController.getZonedFormatter().print(getEndMillis() + 1000)) + (hashSetCountsString.isEmpty() ? "" : Bundle.EventNodeBase_toolTip_hashSetHits(hashSetCountsString)) + (tagCountsString.isEmpty() ? "" : Bundle.EventNodeBase_toolTip_tags(tagCountsString)); } - + @Override protected void succeeded() { super.succeeded(); @@ -316,10 +318,10 @@ public abstract class EventNodeBase extends StackPan } }; new Thread(tooltTipTask).start(); - chart.getController().monitorTask(tooltTipTask); + chartLane.getController().monitorTask(tooltTipTask); } } - + void enableTooltip(boolean toolTipEnabled) { if (toolTipEnabled) { Tooltip.install(this, tooltip); @@ -327,27 +329,27 @@ public abstract class EventNodeBase extends StackPan Tooltip.uninstall(this, tooltip); } } - + final EventType getEventType() { return tlEvent.getEventType(); } - + long getStartMillis() { return tlEvent.getStartMillis(); } - + final long getEndMillis() { return tlEvent.getEndMillis(); } - + final double getLayoutXCompensation() { return parentNode != null - ? getChart().getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis())) + ? getChartLane().getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis())) : 0; } - + abstract String getDescription(); - + void animateTo(double xLeft, double yTop) { if (timeline != null) { timeline.stop(); @@ -360,15 +362,15 @@ public abstract class EventNodeBase extends StackPan timeline.setOnFinished(finished -> Platform.runLater(this::requestChartLayout)); timeline.play(); } - + abstract void requestChartLayout(); - + void setDescriptionVisibility(DescriptionVisibility get) { descVisibility.set(get); } - + abstract void setDescriptionVisibiltiyImpl(DescriptionVisibility get); - + boolean hasDescription(String other) { return this.getDescription().startsWith(other); } @@ -387,23 +389,23 @@ public abstract class EventNodeBase extends StackPan setBackground(defaultBackground); } } - + void applyHighlightEffect() { applyHighlightEffect(true); } - + void clearHighlightEffect() { applyHighlightEffect(false); } - + abstract Collection getEventIDs(); - + abstract EventHandler getDoubleClickHandler(); - + abstract Collection getActions(); - + static class PinEventAction extends Action { - + @NbBundle.Messages({"PinEventAction.text=Pin"}) PinEventAction(TimeLineController controller, TimeLineEvent event) { super(Bundle.PinEventAction_text()); @@ -411,9 +413,9 @@ public abstract class EventNodeBase extends StackPan setGraphic(new ImageView(PIN)); } } - + static class UnPinEventAction extends Action { - + @NbBundle.Messages({"UnPinEventAction.text=Unpin"}) UnPinEventAction(TimeLineController controller, TimeLineEvent event) { super(Bundle.UnPinEventAction_text()); @@ -426,30 +428,30 @@ public abstract class EventNodeBase extends StackPan * event handler used for mouse events on {@link EventNodeBase}s */ class ClickHandler implements EventHandler { - + private ContextMenu contextMenu; - + @Override public void handle(MouseEvent t) { if (t.getButton() == MouseButton.PRIMARY) { if (t.getClickCount() > 1) { getDoubleClickHandler().handle(t); } else if (t.isShiftDown()) { - chart.getSelectedNodes().add(EventNodeBase.this); + chartLane.getSelectedNodes().add(EventNodeBase.this); } else if (t.isShortcutDown()) { - chart.getSelectedNodes().removeAll(EventNodeBase.this); + chartLane.getSelectedNodes().removeAll(EventNodeBase.this); } else { - chart.getSelectedNodes().setAll(EventNodeBase.this); + chartLane.getSelectedNodes().setAll(EventNodeBase.this); } t.consume(); } else if (t.getButton() == MouseButton.SECONDARY) { - ContextMenu chartContextMenu = chart.getChartContextMenu(t); + ContextMenu chartContextMenu = chartLane.getChartContextMenu(t); if (contextMenu == null) { contextMenu = new ContextMenu(); contextMenu.setAutoHide(true); - + contextMenu.getItems().addAll(ActionUtils.createContextMenu(getActions()).getItems()); - + contextMenu.getItems().add(new SeparatorMenuItem()); contextMenu.getItems().addAll(chartContextMenu.getItems()); } @@ -457,7 +459,7 @@ public abstract class EventNodeBase extends StackPan t.consume(); } } - + } - + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 100be6b0d2..9485ab334b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -33,7 +33,7 @@ import org.controlsfx.control.action.ActionUtils; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; -import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChart.HideDescriptionAction; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChartLane.HideDescriptionAction; import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configureActionButton; /** @@ -44,14 +44,13 @@ final public class EventStripeNode extends MultiEventNodeBase chartLane, EventStripe eventStripe, EventClusterNode parentNode) { + super(chartLane, eventStripe, parentNode); setMinHeight(24); //setup description label - + descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); - + descrLabel.setPrefWidth(USE_COMPUTED_SIZE); setAlignment(subNodePane, Pos.BOTTOM_LEFT); @@ -64,10 +63,10 @@ final public class EventStripeNode extends MultiEventNodeBase childNode; EventCluster cluster = Iterables.getOnlyElement(eventStripe.getClusters()); if (cluster.getEventIDs().size() == 1) { - SingleEventNode singleEventNode = new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); + SingleEventNode singleEventNode = new SingleEventNode(getChartLane(), getChartLane().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); childNode = singleEventNode; } else { - EventClusterNode eventClusterNode = new EventClusterNode(getChart(), cluster, this); + EventClusterNode eventClusterNode = new EventClusterNode(getChartLane(), cluster, this); eventClusterNode.installActionButtons(); eventClusterNode.infoHBox.getChildren().remove(eventClusterNode.countLabel); controlsHBox.getChildren().addAll(eventClusterNode.minusButton, eventClusterNode.plusButton); @@ -84,13 +83,11 @@ final public class EventStripeNode extends MultiEventNodeBase createChildNode(EventCluster cluster) { if (cluster.getEventIDs().size() == 1) { - return new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); + return new SingleEventNode(getChartLane(), getChartLane().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); } else { - return new EventClusterNode(getChart(), cluster, this); + return new EventClusterNode(getChartLane(), cluster, this); } } - - @Override void setDescriptionVisibiltiyImpl(DescriptionVisibility descrVis) { final int size = getEventStripe().getSize(); @@ -142,6 +137,6 @@ final public class EventStripeNode extends MultiEventNodeBase getActions() { - return Arrays.asList(new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chart)); + return Arrays.asList(new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chartLane)); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java index a462a92b9e..fe7db93425 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java @@ -38,7 +38,7 @@ class GuideLine extends Line { private static final Tooltip CHART_DEFAULT_TOOLTIP = AbstractVisualizationPane.getDefaultTooltip(); private final Tooltip tooltip = new Tooltip(); - private final EventDetailsChart chart; + private final DetailViewPane chart; //used across invocations of mouse event handlers to maintain state private double startLayoutX; @@ -47,7 +47,7 @@ class GuideLine extends Line { /** * @param chart the chart this GuideLine belongs to. */ - GuideLine(EventDetailsChart chart) { + GuideLine(DetailViewPane chart) { super(0, 0, 0, 0); this.chart = chart; Axis xAxis = chart.getXAxis(); @@ -65,7 +65,7 @@ class GuideLine extends Line { setOnMouseClicked(clickedEvent -> { if (clickedEvent.getButton() == MouseButton.SECONDARY && clickedEvent.isStillSincePress() == false) { - chart.clearGuideLine(); + chart.clearGuideLine(this); clickedEvent.consume(); } }); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java index 6e5a74710f..dc381ff142 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java @@ -66,7 +66,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent chart, BundleType eventBundle, ParentNodeType parentNode) { super(eventBundle, parentNode, chart); this.descLOD.set(eventBundle.getDescriptionLoD()); @@ -101,7 +101,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent { - Tooltip.uninstall(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); + Tooltip.uninstall(chart, AbstractVisualizationPane.getDefaultTooltip()); showHoverControls(true); toFront(); }); @@ -110,7 +110,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent Platform.runLater(chart::requestTimelineChartLayout)); + timeline.setOnFinished(finished -> Platform.runLater(chartLane::requestTimelineChartLayout)); timeline.play(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java index fe4840b27b..e898ab11b4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java @@ -18,96 +18,21 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import com.google.common.collect.Range; -import com.google.common.collect.TreeRangeMap; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; -import java.util.MissingResourceException; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; -import javafx.beans.property.ReadOnlyDoubleProperty; -import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.SetChangeListener; -import javafx.geometry.Insets; -import javafx.scene.Cursor; -import javafx.scene.Group; -import javafx.scene.Node; -import javafx.scene.Scene; import javafx.scene.chart.Axis; -import javafx.scene.chart.XYChart; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.Tooltip; -import javafx.scene.input.MouseEvent; -import static javafx.scene.layout.Region.USE_PREF_SIZE; -import org.controlsfx.control.action.ActionUtils; import org.joda.time.DateTime; -import org.sleuthkit.autopsy.coreutils.ThreadConfined; -import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; -import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; -import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; -import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; /** * */ -public final class PinnedEventsChart extends XYChart implements DetailsChart { - - private static final String styleSheet = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS - private static final int MINIMUM_EVENT_NODE_GAP = 4; - private static final int MINIMUM_ROW_HEIGHT = 24; - - private static EventNodeBase createNode(PinnedEventsChart chart, TimeLineEvent event) { - if (event instanceof SingleEvent) { - return new SingleEventNode(chart, (SingleEvent) event, null); - } else if (event instanceof EventCluster) { - return new EventClusterNode(chart, (EventCluster) event, null); - } else { - return new EventStripeNode(chart, (EventStripe) event, null); - } - } - private Map> eventMap = new HashMap<>(); - private ContextMenu chartContextMenu; - - private final TimeLineController controller; - private final FilteredEventsModel filteredEvents; - /** - * the group that all event nodes are added to. This facilitates scrolling - * by allowing a single translation of this group. - */ - private final Group nodeGroup = new Group(); - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - private final ObservableList events = FXCollections.observableArrayList(); - private final ObservableList< EventNodeBase> eventNodes = FXCollections.observableArrayList(); - private final ObservableList< EventNodeBase> sortedEventNodes = eventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)); - private double descriptionWidth; - - /** - * the maximum y value used so far during the most recent layout pass - */ - private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); - private final ObservableList> selectedNodes; - private final DetailViewLayoutSettings layoutSettings; - /** - * listener that triggers chart layout pass - */ - private final InvalidationListener layoutInvalidationListener = (Observable o) -> { - layoutPlotChildren(); - }; +public final class PinnedEventsChart extends DetailsChartLane { /** * @@ -116,58 +41,19 @@ public final class PinnedEventsChart extends XYChart im * @param verticalAxis the value of verticalAxis * @param selectedNodes1 the value of selectedNodes1 */ - PinnedEventsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes, DetailViewLayoutSettings layoutSettings) { - super(dateAxis, verticalAxis); - this.layoutSettings = layoutSettings; - this.controller = controller; - this.filteredEvents = this.controller.getEventsModel(); + PinnedEventsChart(DetailViewPane parentPane, DateAxis dateAxis, final Axis verticalAxis) { + super(parentPane, dateAxis, verticalAxis, false); - sceneProperty().addListener(observable -> { - Scene scene = getScene(); - if (scene != null && scene.getStylesheets().contains(styleSheet) == false) { - scene.getStylesheets().add(styleSheet); - } - }); - - filteredEvents.zoomParametersProperty().addListener(o -> { - }); - - Tooltip.install(this, AbstractVisualizationPane.getDefaultTooltip()); - - dateAxis.setAutoRanging(false); - verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm - verticalAxis.setTickLabelsVisible(false); - verticalAxis.setTickMarkVisible(false); - setLegendVisible(false); - setPadding(Insets.EMPTY); - setAlternativeColumnFillVisible(true); - - //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly - getPlotChildren().add(nodeGroup); final Series series = new Series<>(); setData(FXCollections.observableArrayList()); getData().add(series); -// //add listener for events that should trigger layout - layoutSettings.bandByTypeProperty().addListener(layoutInvalidationListener); - layoutSettings.oneEventPerRowProperty().addListener(layoutInvalidationListener); - layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); - layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); - layoutSettings.descrVisibilityProperty().addListener(layoutInvalidationListener); - getController().getQuickHideFilters().addListener(layoutInvalidationListener); -// getController().getQuickHideFilters().addListener(layoutInvalidationListener); // //this is needed to allow non circular binding of the guideline and timerangeRect heights to the height of the chart // //TODO: seems like a hack, can we remove? -jm // boundsInLocalProperty().addListener((Observable observable) -> { // setPrefHeight(boundsInLocalProperty().get().getHeight()); // }); -// ChartDragHandler chartDragHandler = new ChartDragHandler<>(this); -// setOnMousePressed(chartDragHandler); -// setOnMouseReleased(chartDragHandler); -// setOnMouseDragged(chartDragHandler); -// -// setOnMouseClicked(new MouseClickedHandler<>(this)); - controller.getPinnedEvents().addListener((SetChangeListener.Change change) -> { + getController().getPinnedEvents().addListener((SetChangeListener.Change change) -> { if (change.wasAdded()) { TimeLineEvent elementAdded = change.getElementAdded(); Data data1 = new Data<>(new DateTime(elementAdded.getStartMillis()), elementAdded); @@ -183,30 +69,7 @@ public final class PinnedEventsChart extends XYChart im requestChartLayout(); }); - this.selectedNodes = selectedNodes; - } - @Override - public ContextMenu getChartContextMenu() { - return chartContextMenu; - } - - @Override - public ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException { - if (chartContextMenu != null) { - chartContextMenu.hide(); - } - - chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(//new EventDetailsChart.PlaceMarkerAction(clickEvent), - TimeLineChart.newZoomHistoyActionGroup(controller))); - chartContextMenu.setAutoHide(true); - return chartContextMenu; - } -// final ObservableList> selectedNodes; - - @Override - public Node asNode() { - return this; } @Override @@ -214,136 +77,63 @@ public final class PinnedEventsChart extends XYChart im return FXCollections.emptyObservableList(); } - @Override - public IntervalSelector getIntervalSelector() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public void setIntervalSelector(IntervalSelector newIntervalSelector) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public IntervalSelector newIntervalSelector() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public void clearIntervalSelector() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public TimeLineController getController() { - return controller; - } - - @Override - public void requestTimelineChartLayout() { - requestChartLayout(); - } - - @Override - public ObservableList> getSelectedNodes() { - return selectedNodes; - } - - @Override - public double layoutEventBundleNodes(final Collection> nodes, final double minY) { - // map from y-ranges to maximum x - TreeRangeMap maxXatY = TreeRangeMap.create(); - - // maximum y values occupied by any of the given nodes, updated as nodes are layed out. - double localMax = minY; - - //for each node do a recursive layout to size it and then position it in first available slot - for (EventNodeBase eventNode : nodes) { - //is the node hiden by a quick hide filter? - - layoutBundleHelper(eventNode); - //get computed height and width - double h = eventNode.getBoundsInLocal().getHeight(); - double w = eventNode.getBoundsInLocal().getWidth(); - //get left and right x coords from axis plus computed width - double xLeft = getXForEpochMillis(eventNode.getStartMillis()) - eventNode.getLayoutXCompensation(); - double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP; - - //initial test position - double yTop = (layoutSettings.getOneEventPerRow()) - ? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end - : computeYTop(minY, h, maxXatY, xLeft, xRight); - - localMax = Math.max(yTop + h, localMax); - - if ((xLeft != eventNode.getLayoutX()) || (yTop != eventNode.getLayoutY())) { - //animate node to new position - eventNode.animateTo(xLeft, yTop); - } - - } - return localMax; //return new max - } - - @Override - protected void dataItemAdded(Series series, int itemIndex, Data item) { - } - - @Override - protected void dataItemRemoved(Data item, Series series) { - } - - @Override - protected void dataItemChanged(Data item) { - } - - @Override - protected void seriesAdded(Series series, int seriesIndex) { - } - - @Override - protected void seriesRemoved(Series series) { - } - - @Override - protected void layoutPlotChildren() { - setCursor(Cursor.WAIT); - maxY.set(0); - -// //These don't change during a layout pass and are expensive to compute per node. So we do it once at the start -// activeQuickHidefilters = getController().getQuickHideFilters().stream() -// .filter(AbstractFilter::isActive) -// .map(DescriptionFilter::getDescription) -// .collect(Collectors.toSet()); - //This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start - descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE; - - if (layoutSettings.getBandByType()) { - sortedEventNodes.stream() - .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() - .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); - } else { - maxY.set(layoutEventBundleNodes(sortedEventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); - } - setCursor(null); - } - - /** - * expose as protected - */ - @Override - protected void requestChartLayout() { - super.requestChartLayout(); - } - - public synchronized void setVScroll(double vScrollValue) { - nodeGroup.setTranslateY(-vScrollValue); - } - - public ReadOnlyDoubleProperty maxVScrollProperty() { - return maxY.getReadOnlyProperty(); - } - +// @Override +// public double layoutEventBundleNodes(final Collection> nodes, final double minY) { +// // map from y-ranges to maximum x +// TreeRangeMap maxXatY = TreeRangeMap.create(); +// +// // maximum y values occupied by any of the given nodes, updated as nodes are layed out. +// double localMax = minY; +// +// //for each node do a recursive layout to size it and then position it in first available slot +// for (EventNodeBase eventNode : nodes) { +// //is the node hiden by a quick hide filter? +// +// layoutBundleHelper(eventNode); +// //get computed height and width +// double h = eventNode.getBoundsInLocal().getHeight(); +// double w = eventNode.getBoundsInLocal().getWidth(); +// //get left and right x coords from axis plus computed width +// double xLeft = getXForEpochMillis(eventNode.getStartMillis()) - eventNode.getLayoutXCompensation(); +// double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP; +// +// //initial test position +// double yTop = (layoutSettings.getOneEventPerRow()) +// ? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end +// : computeYTop(minY, h, maxXatY, xLeft, xRight); +// +// localMax = Math.max(yTop + h, localMax); +// +// if ((xLeft != eventNode.getLayoutX()) || (yTop != eventNode.getLayoutY())) { +// //animate node to new position +// eventNode.animateTo(xLeft, yTop); +// } +// +// } +// return localMax; //return new max +// } +// @Override +// protected void layoutPlotChildren() { +// setCursor(Cursor.WAIT); +// maxY.set(0); +// +//// //These don't change during a layout pass and are expensive to compute per node. So we do it once at the start +//// activeQuickHidefilters = getController().getQuickHideFilters().stream() +//// .filter(AbstractFilter::isActive) +//// .map(DescriptionFilter::getDescription) +//// .collect(Collectors.toSet()); +// //This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start +// descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE; +// +// if (layoutSettings.getBandByType()) { +// sortedNodes.stream() +// .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() +// .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); +// } else { +// maxY.set(layoutEventBundleNodes(sortedNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); +// } +// setCursor(null); +// } /** * add a dataitem to this chart * @@ -358,31 +148,21 @@ public final class PinnedEventsChart extends XYChart im eventMap.put(event, eventNode); Platform.runLater(() -> { events.add(event); - eventNodes.add(eventNode); + nodes.add(eventNode); nodeGroup.getChildren().add(eventNode); data.setNode(eventNode); }); } - /** - * @return all the nodes that pass the given predicate - */ - synchronized Iterable> getNodes(Predicate> p) { - //use this recursive function to flatten the tree of nodes into an single stream. - Function, Stream>> stripeFlattener = - new Function, Stream>>() { - @Override - public Stream> apply(EventNodeBase node) { - return Stream.concat( - Stream.of(node), - node.getSubNodes().stream().flatMap(this::apply)); - } - }; - - return sortedEventNodes.stream() - .flatMap(stripeFlattener) - .filter(p).collect(Collectors.toList()); + static EventNodeBase createNode(DetailsChartLane chart, TimeLineEvent event) { + if (event instanceof SingleEvent) { + return new SingleEventNode(chart, (SingleEvent) event, null); + } else if (event instanceof EventCluster) { + return new EventClusterNode(chart, (EventCluster) event, null); + } else { + return new EventStripeNode(chart, (EventStripe) event, null); + } } /** @@ -396,50 +176,14 @@ public final class PinnedEventsChart extends XYChart im EventNodeBase removedNode = eventMap.remove(data.getYValue()); Platform.runLater(() -> { events.removeAll(data.getYValue()); - eventNodes.removeAll(removedNode); + nodes.removeAll(removedNode); nodeGroup.getChildren().removeAll(removedNode); data.setNode(null); }); } - private double getXForEpochMillis(Long millis) { - DateTime dateTime = new DateTime(millis); - return getXAxis().getDisplayPosition(dateTime); + @Override + void doAdditionalLayout() { } - private double computeYTop(double yMin, double h, TreeRangeMap maxXatY, double xLeft, double xRight) { - double yTop = yMin; - double yBottom = yTop + h; - //until the node is not overlapping any others try moving it down. - boolean overlapping = true; - while (overlapping) { - overlapping = false; - //check each pixel from bottom to top. - for (double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) { - final Double maxX = maxXatY.get(y); - if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) { - //if that pixel is already used - //jump top to this y value and repeat until free slot is found. - overlapping = true; - yTop = y + MINIMUM_EVENT_NODE_GAP; - yBottom = yTop + h; - break; - } - } - } - maxXatY.put(Range.closed(yTop, yBottom), xRight); - return yTop; - } - - private void layoutBundleHelper(final EventNodeBase eventNode) { - //make sure it is shown - eventNode.setVisible(true); - eventNode.setManaged(true); - //apply advanced layout description visibility options - eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility()); - eventNode.setMaxDescriptionWidth(descriptionWidth); - - //do recursive layout - eventNode.layoutChildren(); - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java new file mode 100644 index 0000000000..06a871ffd1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java @@ -0,0 +1,163 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-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.detailview; + +import com.google.common.collect.Iterables; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import javafx.application.Platform; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.scene.chart.Axis; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.shape.Line; +import javafx.scene.shape.StrokeLineCap; +import org.joda.time.DateTime; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; + +/** + * Custom implementation of {@link XYChart} to graph events on a horizontal + * timeline. + * + * The horizontal {@link DateAxis} controls the tick-marks and the horizontal + * layout of the nodes representing events. The vertical {@link NumberAxis} does + * nothing (although a custom implementation could help with the vertical + * layout?) + * + * Series help organize events for the banding by event type, we could add a + * node to contain each band if we need a place for per band controls. + * + * //TODO: refactor the projected lines to a separate class. -jm + */ +public final class PrimaryDetailsChart extends DetailsChartLane { + + private static final int PROJECTED_LINE_Y_OFFSET = 5; + private static final int PROJECTED_LINE_STROKE_WIDTH = 5; + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private final Map projectionMap = new ConcurrentHashMap<>(); + + PrimaryDetailsChart(DetailViewPane parentPane, DateAxis dateAxis, final Axis verticalAxis) { + super(parentPane, dateAxis, verticalAxis, true); + +// filteredEvents.zoomParametersProperty().addListener(o -> { +// selectedNodes.clear(); +// projectionMap.clear(); +// controller.selectEventIDs(Collections.emptyList()); +// }); +// //this is needed to allow non circular binding of the guideline and timerangeRect heights to the height of the chart +// //TODO: seems like a hack, can we remove? -jm +// boundsInLocalProperty().addListener((Observable observable) -> { +// setPrefHeight(boundsInLocalProperty().get().getHeight()); +// }); + //add listener for events that should trigger layout + getController().getQuickHideFilters().addListener(layoutInvalidationListener); + + selectedNodes.addListener((ListChangeListener.Change> change) -> { + while (change.next()) { + change.getRemoved().forEach(removedNode -> { + removedNode.getEvent().getClusters().forEach(cluster -> { + Line removedLine = projectionMap.remove(cluster); + getChartChildren().removeAll(removedLine); + }); + + }); + change.getAddedSubList().forEach(addedNode -> { + for (EventCluster range : addedNode.getEvent().getClusters()) { + double y = dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET; + Line line = + new Line(dateAxis.localToParent(getXForEpochMillis(range.getStartMillis()), 0).getX(), y, + dateAxis.localToParent(getXForEpochMillis(range.getEndMillis()), 0).getX(), y); + line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5)); + line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); + line.setStrokeLineCap(StrokeLineCap.ROUND); + projectionMap.put(range, line); + getChartChildren().add(line); + } + }); + } + }); + } + + public ObservableList getEventStripes() { + return events; + } + + /** + * add a dataitem to this chart + * + * @see note in main section of class JavaDoc + * + * @param data + */ + void addDataItem(Data data) { + final EventStripe eventStripe = data.getYValue(); + EventNodeBase newNode; + if (eventStripe.getEventIDs().size() == 1) { + newNode = new SingleEventNode(this, controller.getEventsModel().getEventById(Iterables.getOnlyElement(eventStripe.getEventIDs())), null); + } else { + newNode = new EventStripeNode(PrimaryDetailsChart.this, eventStripe, null); + } + Platform.runLater(() -> { + events.add(eventStripe); + nodes.add(newNode); + nodeGroup.getChildren().add(newNode); + data.setNode(newNode); + }); + } + + /** + * remove a data item from this chart + * + * @see note in main section of class JavaDoc + * + * @param data + */ + void removeDataItem(Data data) { + Platform.runLater(() -> { + EventNodeBase removedNode = (EventNodeBase) data.getNode(); + events.removeAll(new StripeFlattener().apply(removedNode).collect(Collectors.toList())); + nodes.removeAll(removedNode); + nodeGroup.getChildren().removeAll(removedNode); + data.setNode(null); + }); + } + + private double getParentXForEpochMillis(Long epochMillis) { + return getXAxis().localToParent(getXForEpochMillis(epochMillis), 0).getX(); + } + + void doAdditionalLayout() { + for (final Map.Entry entry : projectionMap.entrySet()) { + final EventCluster cluster = entry.getKey(); + final Line line = entry.getValue(); + + line.setStartX(getParentXForEpochMillis(cluster.getStartMillis())); + line.setEndX(getParentXForEpochMillis(cluster.getEndMillis())); + + line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); + line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingWrapper.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingLaneWrapper.java similarity index 79% rename from Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingWrapper.java rename to Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingLaneWrapper.java index 11ca4fe780..67d569351b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingWrapper.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingLaneWrapper.java @@ -6,35 +6,31 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import javafx.application.Platform; -import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.geometry.Orientation; -import javafx.scene.chart.XYChart; import javafx.scene.control.ScrollBar; import javafx.scene.input.KeyEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; -import org.joda.time.DateTime; -import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; /** * */ -public class ScrollingWrapper & DetailsChart> extends BorderPane { +class ScrollingLaneWrapper extends BorderPane { private static final double LINE_SCROLL_PERCENTAGE = .10; private static final double PAGE_SCROLL_PERCENTAGE = .70; private final ScrollBar vertScrollBar = new ScrollBar(); private final Region scrollBarSpacer = new Region(); - private final ChartType chart; + private final DetailsChartLane chart; - public ScrollingWrapper(ChartType center) { + ScrollingLaneWrapper(DetailsChartLane center) { super(center); this.chart = center; - scrollBarSpacer.minHeightProperty().bind(((XYChart) chart).getXAxis().heightProperty()); + scrollBarSpacer.minHeightProperty().bind(chart.getXAxis().heightProperty()); //configure scrollbar vertScrollBar.setOrientation(Orientation.VERTICAL); @@ -45,16 +41,13 @@ public class ScrollingWrapper { + chart.setVScroll(vertScrollBar.getValue()); }); //request focus for keyboard scrolling setOnMouseClicked(mouseEvent -> requestFocus()); -//interpret scroll events to the scrollBar + //interpret scroll events to the scrollBar this.setOnScroll(scrollEvent -> vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() - scrollEvent.getDeltaY()))); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java index daf89d9a9c..cb7bafd1ce 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java @@ -35,11 +35,9 @@ import javafx.scene.layout.CornerRadii; import static javafx.scene.layout.Region.USE_PREF_SIZE; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.action.Action; -import org.joda.time.DateTime; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; -import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; /** * @@ -62,7 +60,7 @@ final class SingleEventNode extends EventNodeBase { @Override Collection getActions() { - TimeLineController controller = getChart().getController(); + TimeLineController controller = getChartLane().getController(); if (controller.getPinnedEvents().contains(tlEvent)) { return Arrays.asList(new UnPinEventAction(controller, tlEvent)); } else { @@ -70,7 +68,7 @@ final class SingleEventNode extends EventNodeBase { } } - SingleEventNode(DetailsChart chart, SingleEvent event, MultiEventNodeBase parent) { + SingleEventNode(DetailsChartLane chart, SingleEvent event, MultiEventNodeBase parent) { super(event, parent, chart); this.descrLabel.setText(event.getFullDescription()); eventTypeImageView.setImage(getEventType().getFXImage()); @@ -94,10 +92,6 @@ final class SingleEventNode extends EventNodeBase { getChildren().add(infoHBox); } - @Override - public TimeLineChart getChart() { - return chart; - } @Override public List> getSubNodes() { @@ -117,7 +111,7 @@ final class SingleEventNode extends EventNodeBase { @Override void requestChartLayout() { - chart.requestTimelineChartLayout(); + chartLane.requestTimelineChartLayout(); }