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(); }