show pinned events automatically, add labels to pinned and all event swim lanes

move more code into DetailViewPane to allow guideline and interval selector to span the pinned and primary chart lanes

pull common code into DetailsChartLane base class

pull more common code into DetailsChartLane base class

pull even more common code into DetailsChartLane base class
This commit is contained in:
jmillman 2016-03-02 10:28:17 -05:00
parent 651fac80ec
commit 38a6fd3a0f
19 changed files with 1043 additions and 1279 deletions

View File

@ -132,6 +132,10 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
*/
protected List<Node> settingsNodes;
public TimeLineController getController() {
return controller;
}
/**
* @return the list of nodes containing settings widgets to insert into this
* visualization's header

View File

@ -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<X> {
public Axis<X> getXAxis();
public TimeLineController getController();
ContextMenu getContextMenu();
ContextMenu getChartContextMenu(MouseEvent m);
IntervalSelector<? extends X> getIntervalSelector();
void setIntervalSelector(IntervalSelector<? extends X> newIntervalSelector);
/**
* derived classes should implement this so as to supply an appropriate
* subclass of {@link IntervalSelector}
*
* @return a new interval selector
*/
IntervalSelector<X> newIntervalSelector();
/**
* clear any references to previous interval selectors , including removing
* the interval selector from the ui / scene-graph
*/
void clearIntervalSelector();
}

View File

@ -61,7 +61,7 @@ public abstract class IntervalSelector<X> extends BorderPane {
/**
* the Axis this is a selector over
*/
public final TimeLineChart<X> chart;
public final ContextMenuProvider<X> chart;
private Tooltip tooltip;
/////////drag state
@ -89,7 +89,7 @@ public abstract class IntervalSelector<X> extends BorderPane {
@FXML
private BorderPane bottomBorder;
public IntervalSelector(TimeLineChart<X> chart) {
public IntervalSelector(ContextMenuProvider<X> chart) {
this.chart = chart;
this.controller = chart.getController();
FXMLConstructor.construct(this, IntervalSelector.class, "IntervalSelector.fxml"); // NON-NLS

View File

@ -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 <X> the type of values along the horizontal axis
*/
public interface TimeLineChart<X> {
public interface TimeLineChart<X> extends ContextMenuProvider<X> {
// void setController(TimeLineController controller);
IntervalSelector<? extends X> getIntervalSelector();
@ -66,11 +61,6 @@ public interface TimeLineChart<X> {
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<X> {
* @param <X> the type of values along the horizontal axis
* @param <Y> the type of chart this is a drag handler for
*/
static class ChartDragHandler<X, Y extends Chart & TimeLineChart<X>> implements EventHandler<MouseEvent> {
public static class ChartDragHandler<X, Y extends Region & ContextMenuProvider<X>> implements EventHandler<MouseEvent> {
private final Y chart;
@ -102,8 +92,7 @@ public interface TimeLineChart<X> {
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<X> {
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<X> {
}
static class MouseClickedHandler<X, C extends Chart & TimeLineChart<X>> implements EventHandler<MouseEvent> {
static class MouseClickedHandler<X, C extends Region & ContextMenuProvider<X>> implements EventHandler<MouseEvent> {
private final C chart;
@ -131,15 +119,15 @@ public interface TimeLineChart<X> {
}
@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();
}
}
}

View File

@ -65,10 +65,6 @@ final class EventCountsChart extends StackedBarChart<String, Number> 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<String, Number> implements
*
* @return the context menu for this chart
*/
ContextMenu getContextMenu() {
public ContextMenu getContextMenu() {
return chartContextMenu;
}

View File

@ -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<DateTime, EventStripe, EventNodeBase<?>, EventDetailsChart> {
public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStripe, EventNodeBase<?>, PrimaryDetailsChart> implements ContextMenuProvider<DateTime> {
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<EventStripe> verticalAxis = new EventAxis<>();
private final Axis<EventStripe> verticalAxis = new EventAxis<>("All Events");
private MultipleSelectionModel<TreeItem<TimeLineEvent>> treeSelectionModel;
private final ObservableList<EventNodeBase<?>> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
private final ScrollingWrapper<EventStripe, EventDetailsChart> mainView;
private final ScrollingWrapper<TimeLineEvent, PinnedEventsChart> 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<? extends DateTime> intervalSelector;
private final Pane rootPane;
public ObservableList<EventStripe> getEventStripes() {
return chart.getEventStripes();
}
private static class DetailIntervalSelector extends IntervalSelector<DateTime> {
DetailIntervalSelector(ContextMenuProvider<DateTime> chart) {
super(chart);
}
@Override
protected String formatSpan(DateTime date) {
return date.toString(TimeLineController.getZonedFormatter());
}
@Override
protected Interval adjustInterval(Interval i) {
return i;
}
@Override
protected DateTime parseDateTime(DateTime date) {
return date;
}
}
@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<DateTime, EventStripe> s : dataSeries) {
@ -117,19 +175,22 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
layoutSettings = new DetailViewLayoutSettings();
//initialize chart;
chart = new EventDetailsChart(controller, detailsChartDateAxis, verticalAxis, selectedNodes, layoutSettings);
mainView = new ScrollingWrapper<>(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<DateTime, EventStr
.flatMap(detailNode -> detailNode.getEventIDs().stream())
.collect(Collectors.toList()));
});
filteredEvents.zoomParametersProperty().addListener(o -> {
clearIntervalSelector();
selectedNodes.clear();
controller.selectEventIDs(Collections.emptyList());
});
TimeLineChart.MouseClickedHandler<DateTime, DetailViewPane> mouseClickedHandler = new TimeLineChart.MouseClickedHandler<>(this);
TimeLineChart.ChartDragHandler<DateTime, DetailViewPane> 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<DateTime> newIntervalSelector() {
return new DetailIntervalSelector(this);
}
@Override
public IntervalSelector<? extends DateTime> getIntervalSelector() {
return intervalSelector;
}
@Override
public void setIntervalSelector(IntervalSelector<? extends DateTime> newIntervalSelector) {
intervalSelector = newIntervalSelector;
rootPane.getChildren().add(getIntervalSelector());
}
public void setSelectionModel(MultipleSelectionModel<TreeItem<TimeLineEvent>> selectionModel) {
@ -188,7 +283,7 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
}
@Override
protected Axis<DateTime> getXAxis() {
public Axis<DateTime> getXAxis() {
return detailsChartDateAxis;
}
@ -215,6 +310,57 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
@Override
protected void applySelectionEffect(EventNodeBase<?> 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<DateTime, DetailViewPane> mouseClickedHandler, final TimeLineChart.ChartDragHandler<DateTime, DetailViewPane> 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<DateTime, EventStr
hiddenRadioMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.hiddenRadioMenuItem.text"));
hiddenRadio.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.hiddenRadio.text"));
pinnedEventsToggle.setOnAction(event -> {
pinnedEventsToggle.selectedProperty().addListener(observable -> {
boolean selected = pinnedEventsToggle.isSelected();
if (selected == false) {
dividerPosition = masterDetailPane.getDividerPosition();
@ -344,7 +490,16 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
masterDetailPane.setDividerPosition(dividerPosition);
}
});
controller.getPinnedEvents().addListener((SetChangeListener.Change<? extends TimeLineEvent> 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<DateTime, EventStr
public Action newHideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
return new HideDescriptionAction(description, descriptionLoD, chart);
}
@NbBundle.Messages({

View File

@ -1,85 +0,0 @@
/*
* 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 java.util.Collection;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import org.controlsfx.control.action.Action;
import org.joda.time.DateTime;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
/**
*
*/
interface DetailsChart extends TimeLineChart<DateTime> {
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<EventStripe> getEventStripes();
public ObservableList<EventNodeBase<?>> getSelectedNodes();
double layoutEventBundleNodes(final Collection<? extends EventNodeBase<?>> 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))
);
}
}
}

View File

@ -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<Y extends TimeLineEvent> extends XYChart<DateTime, Y> implements TimeLineChart<DateTime> {
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<EventNodeBase<?>> selectedNodes;
final Map<Y, EventNodeBase<?>> eventMap = new HashMap<>();
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
final ObservableList<Y> 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<String> 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<EventNodeBase<?>> 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<DateTime> dateAxis, Axis<Y> 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<? extends EventNodeBase<?>> nodes, final double minY) {
// map from y-ranges to maximum x
TreeRangeMap<Double, Double> 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<EventStripe> getEventStripes();
@Override
public ContextMenu getContextMenu() {
return parentPane.getContextMenu();
}
@Override
protected void dataItemAdded(Series<DateTime, Y> series, int itemIndex, Data<DateTime, Y> item) {
}
@Override
protected void dataItemRemoved(Data<DateTime, Y> item, Series<DateTime, Y> series) {
}
@Override
protected void dataItemChanged(Data<DateTime, Y> item) {
}
@Override
protected void seriesAdded(Series<DateTime, Y> series, int seriesIndex) {
}
@Override
protected void seriesRemoved(Series<DateTime, Y> series) {
}
@Override
public IntervalSelector<? extends DateTime> getIntervalSelector() {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void setIntervalSelector(IntervalSelector<? extends DateTime> newIntervalSelector) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public IntervalSelector<DateTime> 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<EventNodeBase<?>> getNodes(Predicate<EventNodeBase<?>> p) {
//use this recursive function to flatten the tree of nodes into an single stream.
Function<EventNodeBase<?>, Stream<EventNodeBase<?>>> stripeFlattener =
new Function<EventNodeBase<?>, Stream<EventNodeBase<?>>>() {
@Override
public Stream<EventNodeBase<?>> 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<Double, Double> 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))
);
}
}
}

View File

@ -30,6 +30,10 @@ import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
*/
class EventAxis<Type extends TimeLineEvent> extends Axis<Type> {
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.

View File

@ -89,8 +89,8 @@ final public class EventClusterNode extends MultiEventNodeBase<EventCluster, Eve
}
}
EventClusterNode(DetailsChart chart, EventCluster eventCluster, EventStripeNode parentNode) {
super(chart, eventCluster, parentNode);
EventClusterNode(DetailsChartLane<?> 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<EventCluster, Eve
@NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters")
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private synchronized void loadSubBundles(DescriptionLoD.RelativeDetail relativeDetail) {
getChart().asNode().setCursor(Cursor.WAIT);
getChartLane().setCursor(Cursor.WAIT);
/*
@ -206,22 +206,22 @@ final public class EventClusterNode extends MultiEventNodeBase<EventCluster, Eve
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); //NON-NLS
}
getChart().requestTimelineChartLayout();
getChart().asNode().setCursor(null);
getChartLane().requestTimelineChartLayout();
getChartLane().setCursor(null);
}
};
new Thread(loggedTask).start();
//start task
getChart().getController().monitorTask(loggedTask);
getChartLane().getController().monitorTask(loggedTask);
}
@Override
EventNodeBase<?> 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<EventCluster, Eve
@Override
protected void layoutChildren() {
double chartX = getChart().getXAxis().getDisplayPosition(new DateTime(getStartMillis()));
double w = getChart().getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX;
double chartX = getChartLane().getXAxis().getDisplayPosition(new DateTime(getStartMillis()));
double w = getChartLane().getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX;
subNodePane.setPrefWidth(Math.max(1, w));
super.layoutChildren();
}

View File

@ -1,683 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-15 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import 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<DateTime, EventStripe> 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<String> 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<? extends DateTime> 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<EventNodeBase<?>> 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<EventStripe> eventStripes = FXCollections.observableArrayList();
private final ObservableList< EventNodeBase<?>> stripeNodes = FXCollections.observableArrayList();
private final ObservableList< EventNodeBase<?>> sortedStripeNodes = stripeNodes.sorted(Comparator.comparing(EventNodeBase<?>::getStartMillis));
private final Map<EventCluster, Line> projectionMap = new ConcurrentHashMap<>();
EventDetailsChart(TimeLineController controller, DateAxis dateAxis, final Axis<EventStripe> verticalAxis, ObservableList<EventNodeBase<?>> 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<DateTime, EventDetailsChart> 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<EventNodeBase<?>> getSelectedNodes() {
return selectedNodes;
}
@Override
public Node asNode() {
return this;
}
public ObservableList<EventStripe> 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<DateTime> 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<? extends DateTime> getIntervalSelector() {
return intervalSelector;
}
@Override
public void setIntervalSelector(IntervalSelector<? extends DateTime> newIntervalSelector) {
intervalSelector = newIntervalSelector;
getChartChildren().add(getIntervalSelector());
}
/**
* @see note in main section of class JavaDoc
*
* @param series
* @param i
*/
@Override
protected void seriesAdded(Series<DateTime, EventStripe> series, int i) {
}
/**
* @see note in main section of class JavaDoc
*
* @param series
*/
@Override
protected void seriesRemoved(Series<DateTime, EventStripe> series) {
}
/**
* @see note in main section of class JavaDoc
*
* @param series
* @param itemIndex
* @param item
*/
@Override
protected void dataItemAdded(Series<DateTime, EventStripe> series, int itemIndex, Data<DateTime, EventStripe> item) {
}
/**
* @see note in main section of class JavaDoc
*
*
* @param item
* @param series
*/
@Override
protected void dataItemRemoved(Data<DateTime, EventStripe> item, Series<DateTime, EventStripe> series) {
}
/**
* @see note in main section of class JavaDoc
*
* @param item
*/
@Override
protected void dataItemChanged(Data<DateTime, EventStripe> item) {
}
/**
* add a dataitem to this chart
*
* @see note in main section of class JavaDoc
*
* @param data
*/
void addDataItem(Data<DateTime, EventStripe> 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<DateTime, EventStripe> 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<EventNodeBase<?>> getNodes(Predicate<EventNodeBase<?>> p) {
//use this recursive function to flatten the tree of nodes into an single stream.
Function<EventNodeBase<?>, Stream<EventNodeBase<?>>> stripeFlattener =
new Function<EventNodeBase<?>, Stream<EventNodeBase<?>>>() {
@Override
public Stream<EventNodeBase<?>> 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<? extends EventNodeBase<?>> nodes, final double minY) {
// map from y-ranges to maximum x
TreeRangeMap<Double, Double> 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<Double, Double> 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<EventCluster, Line> 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<DateTime> {
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<EventNodeBase<?>> {
private final Axis<DateTime> dateAxis;
SelectionChangeHandler() {
dateAxis = getXAxis();
}
@Override
public void onChanged(ListChangeListener.Change<? extends EventNodeBase<?>> 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);
}
});
}
}
}
}

View File

@ -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<Type extends TimeLineEvent> extends StackPane {
static final Map<EventType, Effect> 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<Type extends TimeLineEvent> 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<DescriptionLoD> descLOD = new SimpleObjectProperty<>();
final SimpleObjectProperty<DescriptionVisibility> 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<Type extends TimeLineEvent> 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<Type extends TimeLineEvent> 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<DateTime> getChart();
DetailsChartLane<?> getChartLane() {
return chartLane;
}
/**
* @param w the maximum width the description label should have
@ -187,7 +189,7 @@ public abstract class EventNodeBase<Type extends TimeLineEvent> extends StackPan
public void setMaxDescriptionWidth(double w) {
descrLabel.setMaxWidth(w);
}
public abstract List<EventNodeBase<?>> getSubNodes();
/**
@ -198,7 +200,7 @@ public abstract class EventNodeBase<Type extends TimeLineEvent> 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<Type extends TimeLineEvent> 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<Type extends TimeLineEvent> 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<Type extends TimeLineEvent> extends StackPan
});
pinButton.setGraphic(new ImageView(PIN));
}
show(controlsHBox, showControls);
if (parentNode != null) {
parentNode.showHoverControls(false);
@ -268,7 +270,7 @@ public abstract class EventNodeBase<Type extends TimeLineEvent> extends StackPan
{
updateTitle(Bundle.EventNodeBase_toolTip_loading2());
}
@Override
protected String call() throws Exception {
HashMap<String, Long> hashSetCounts = new HashMap<>();
@ -288,7 +290,7 @@ public abstract class EventNodeBase<Type extends TimeLineEvent> extends StackPan
String hashSetCountsString = hashSetCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
Map<String, Long> tagCounts = new HashMap<>();
if (tlEvent.getEventIDsWithTags().isEmpty() == false) {
tagCounts.putAll(eventsModel.getTagCountsByTagName(tlEvent.getEventIDsWithTags()));
@ -296,14 +298,14 @@ public abstract class EventNodeBase<Type extends TimeLineEvent> extends StackPan
String tagCountsString = tagCounts.entrySet().stream()
.map((Map.Entry<String, Long> 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<Type extends TimeLineEvent> 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<Type extends TimeLineEvent> 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<Type extends TimeLineEvent> 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<Type extends TimeLineEvent> extends StackPan
setBackground(defaultBackground);
}
}
void applyHighlightEffect() {
applyHighlightEffect(true);
}
void clearHighlightEffect() {
applyHighlightEffect(false);
}
abstract Collection<Long> getEventIDs();
abstract EventHandler<MouseEvent> getDoubleClickHandler();
abstract Collection<? extends Action> 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<Type extends TimeLineEvent> 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<Type extends TimeLineEvent> extends StackPan
* event handler used for mouse events on {@link EventNodeBase}s
*/
class ClickHandler implements EventHandler<MouseEvent> {
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<Type extends TimeLineEvent> extends StackPan
t.consume();
}
}
}
}

View File

@ -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<EventStripe, Event
private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName());
private Button hideButton;
EventStripeNode(DetailsChart chart, EventStripe eventStripe, EventClusterNode parentNode) {
super(chart, eventStripe, parentNode);
EventStripeNode(DetailsChartLane<?> 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<EventStripe, Event
EventNodeBase<?> 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<EventStripe, Event
return getEventBundle();
}
@Override
void installActionButtons() {
super.installActionButtons();
if (hideButton == null) {
hideButton = ActionUtils.createButton(new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chart),
if (chartLane.quickHideFiltersEnabled() && hideButton == null) {
hideButton = ActionUtils.createButton(new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chartLane),
ActionUtils.ActionTextBehavior.HIDE);
configureActionButton(hideButton);
@ -101,14 +98,12 @@ final public class EventStripeNode extends MultiEventNodeBase<EventStripe, Event
@Override
EventNodeBase<?> 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<EventStripe, Event
@Override
Collection<? extends Action> getActions() {
return Arrays.asList(new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chart));
return Arrays.asList(new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chartLane));
}
}

View File

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

View File

@ -66,7 +66,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent<ParentTy
private Timeline timeline;
MultiEventNodeBase(DetailsChart chart, BundleType eventBundle, ParentNodeType parentNode) {
MultiEventNodeBase(DetailsChartLane<?> 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<ParentTy
//set up mouse hover effect and tooltip
setOnMouseEntered((MouseEvent e) -> {
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<ParentTy
if (parentNode != null) {
parentNode.showHoverControls(true);
} else {
Tooltip.install(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip());
Tooltip.install(chart, AbstractVisualizationPane.getDefaultTooltip());
}
});
setOnMouseClicked(new ClickHandler());
@ -120,12 +120,9 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent<ParentTy
@Override
void requestChartLayout() {
getChart().requestTimelineChartLayout();
getChartLane().requestTimelineChartLayout();
}
public DetailsChart getChart() {
return chart;
}
final DescriptionLoD getDescriptionLoD() {
return descLOD.get();
@ -159,7 +156,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent<ParentTy
@Override
protected void layoutChildren() {
chart.layoutEventBundleNodes(subNodes, 0);
chartLane.layoutEventBundleNodes(subNodes, 0);
super.layoutChildren();
}
@ -168,13 +165,13 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent<ParentTy
void animateTo(double xLeft, double yTop) {
if (timeline != null) {
timeline.stop();
Platform.runLater(chart::requestTimelineChartLayout);
Platform.runLater(chartLane::requestTimelineChartLayout);
}
timeline = new Timeline(new KeyFrame(Duration.millis(100),
new KeyValue(layoutXProperty(), xLeft),
new KeyValue(layoutYProperty(), yTop))
);
timeline.setOnFinished(finished -> Platform.runLater(chart::requestTimelineChartLayout));
timeline.setOnFinished(finished -> Platform.runLater(chartLane::requestTimelineChartLayout));
timeline.play();
}

View File

@ -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<DateTime, TimeLineEvent> 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<TimeLineEvent, EventNodeBase<?>> 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<TimeLineEvent> 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<EventNodeBase<?>> selectedNodes;
private final DetailViewLayoutSettings layoutSettings;
/**
* listener that triggers chart layout pass
*/
private final InvalidationListener layoutInvalidationListener = (Observable o) -> {
layoutPlotChildren();
};
public final class PinnedEventsChart extends DetailsChartLane<TimeLineEvent> {
/**
*
@ -116,58 +41,19 @@ public final class PinnedEventsChart extends XYChart<DateTime, TimeLineEvent> im
* @param verticalAxis the value of verticalAxis
* @param selectedNodes1 the value of selectedNodes1
*/
PinnedEventsChart(TimeLineController controller, DateAxis dateAxis, final Axis<TimeLineEvent> verticalAxis, ObservableList<EventNodeBase<?>> selectedNodes, DetailViewLayoutSettings layoutSettings) {
super(dateAxis, verticalAxis);
this.layoutSettings = layoutSettings;
this.controller = controller;
this.filteredEvents = this.controller.getEventsModel();
PinnedEventsChart(DetailViewPane parentPane, DateAxis dateAxis, final Axis<TimeLineEvent> 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<DateTime, TimeLineEvent> 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<DateTime, EventDetailsChart> chartDragHandler = new ChartDragHandler<>(this);
// setOnMousePressed(chartDragHandler);
// setOnMouseReleased(chartDragHandler);
// setOnMouseDragged(chartDragHandler);
//
// setOnMouseClicked(new MouseClickedHandler<>(this));
controller.getPinnedEvents().addListener((SetChangeListener.Change<? extends TimeLineEvent> change) -> {
getController().getPinnedEvents().addListener((SetChangeListener.Change<? extends TimeLineEvent> change) -> {
if (change.wasAdded()) {
TimeLineEvent elementAdded = change.getElementAdded();
Data<DateTime, TimeLineEvent> data1 = new Data<>(new DateTime(elementAdded.getStartMillis()), elementAdded);
@ -183,30 +69,7 @@ public final class PinnedEventsChart extends XYChart<DateTime, TimeLineEvent> 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<EventNodeBase<?>> selectedNodes;
@Override
public Node asNode() {
return this;
}
@Override
@ -214,136 +77,63 @@ public final class PinnedEventsChart extends XYChart<DateTime, TimeLineEvent> im
return FXCollections.emptyObservableList();
}
@Override
public IntervalSelector<? extends DateTime> getIntervalSelector() {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void setIntervalSelector(IntervalSelector<? extends DateTime> newIntervalSelector) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public IntervalSelector<DateTime> 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<EventNodeBase<?>> getSelectedNodes() {
return selectedNodes;
}
@Override
public double layoutEventBundleNodes(final Collection<? extends EventNodeBase<?>> nodes, final double minY) {
// map from y-ranges to maximum x
TreeRangeMap<Double, Double> 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<DateTime, TimeLineEvent> series, int itemIndex, Data<DateTime, TimeLineEvent> item) {
}
@Override
protected void dataItemRemoved(Data<DateTime, TimeLineEvent> item, Series<DateTime, TimeLineEvent> series) {
}
@Override
protected void dataItemChanged(Data<DateTime, TimeLineEvent> item) {
}
@Override
protected void seriesAdded(Series<DateTime, TimeLineEvent> series, int seriesIndex) {
}
@Override
protected void seriesRemoved(Series<DateTime, TimeLineEvent> 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<? extends EventNodeBase<?>> nodes, final double minY) {
// // map from y-ranges to maximum x
// TreeRangeMap<Double, Double> 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<DateTime, TimeLineEvent> 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<EventNodeBase<?>> getNodes(Predicate<EventNodeBase<?>> p) {
//use this recursive function to flatten the tree of nodes into an single stream.
Function<EventNodeBase<?>, Stream<EventNodeBase<?>>> stripeFlattener =
new Function<EventNodeBase<?>, Stream<EventNodeBase<?>>>() {
@Override
public Stream<EventNodeBase<?>> 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<DateTime, TimeLineEvent> 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<Double, Double> 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();
}
}

View File

@ -0,0 +1,163 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-15 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import 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<EventStripe> {
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<EventCluster, Line> projectionMap = new ConcurrentHashMap<>();
PrimaryDetailsChart(DetailViewPane parentPane, DateAxis dateAxis, final Axis<EventStripe> 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<? extends EventNodeBase<?>> 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<EventStripe> getEventStripes() {
return events;
}
/**
* add a dataitem to this chart
*
* @see note in main section of class JavaDoc
*
* @param data
*/
void addDataItem(Data<DateTime, EventStripe> 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<DateTime, EventStripe> 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<EventCluster, Line> 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);
}
}
}

View File

@ -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<ValueType extends TimeLineEvent, ChartType extends XYChart<DateTime, ValueType> & 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<ValueType extends TimeLineEvent, ChartType extends
setRight(new VBox(vertScrollBar, scrollBarSpacer));
//scrollbar value change handler. This forwards changes in scroll bar to chart
this.vertScrollBar.valueProperty().addListener(new InvalidationListener() {
public void invalidated(Observable observable) {
chart.setVScroll(vertScrollBar.getValue());
}
this.vertScrollBar.valueProperty().addListener((Observable observable) -> {
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())));

View File

@ -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<SingleEvent> {
@Override
Collection<? extends Action> 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<SingleEvent> {
}
}
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<SingleEvent> {
getChildren().add(infoHBox);
}
@Override
public TimeLineChart<DateTime> getChart() {
return chart;
}
@Override
public List<EventNodeBase<?>> getSubNodes() {
@ -117,7 +111,7 @@ final class SingleEventNode extends EventNodeBase<SingleEvent> {
@Override
void requestChartLayout() {
chart.requestTimelineChartLayout();
chartLane.requestTimelineChartLayout();
}