mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-17 10:17:41 +00:00
refactor common code out of clustering modes
This commit is contained in:
parent
2161812bd4
commit
7a85c14b2d
@ -127,26 +127,26 @@ public class EventCluster implements EventBundle {
|
||||
/**
|
||||
* merge two aggregate events into one new aggregate event.
|
||||
*
|
||||
* @param aggEvent1
|
||||
* @param cluster1
|
||||
* @param aggEVent2
|
||||
*
|
||||
* @return a new aggregate event that is the result of merging the given
|
||||
* events
|
||||
*/
|
||||
public static EventCluster merge(EventCluster aggEvent1, EventCluster ag2) {
|
||||
public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) {
|
||||
|
||||
if (aggEvent1.getEventType() != ag2.getEventType()) {
|
||||
if (cluster1.getEventType() != cluster2.getEventType()) {
|
||||
throw new IllegalArgumentException("aggregate events are not compatible they have different types");
|
||||
}
|
||||
|
||||
if (!aggEvent1.getDescription().equals(ag2.getDescription())) {
|
||||
if (!cluster1.getDescription().equals(cluster2.getDescription())) {
|
||||
throw new IllegalArgumentException("aggregate events are not compatible they have different descriptions");
|
||||
}
|
||||
Sets.SetView<Long> idsUnion = Sets.union(aggEvent1.getEventIDs(), ag2.getEventIDs());
|
||||
Sets.SetView<Long> hashHitsUnion = Sets.union(aggEvent1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits());
|
||||
Sets.SetView<Long> taggedUnion = Sets.union(aggEvent1.getEventIDsWithTags(), ag2.getEventIDsWithTags());
|
||||
Sets.SetView<Long> idsUnion = Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs());
|
||||
Sets.SetView<Long> hashHitsUnion = Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits());
|
||||
Sets.SetView<Long> taggedUnion = Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags());
|
||||
|
||||
return new EventCluster(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod);
|
||||
return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod);
|
||||
}
|
||||
|
||||
Range<Long> getRange() {
|
||||
|
@ -60,15 +60,15 @@ public final class EventStripe implements EventBundle {
|
||||
*/
|
||||
private final Set<Long> hashHits = new HashSet<>();
|
||||
|
||||
public EventStripe(EventCluster aggEvent) {
|
||||
spans.add(aggEvent.getRange());
|
||||
spanMap.put(aggEvent.getRange(), aggEvent);
|
||||
type = aggEvent.getEventType();
|
||||
description = aggEvent.getDescription();
|
||||
lod = aggEvent.getDescriptionLOD();
|
||||
eventIDs.addAll(aggEvent.getEventIDs());
|
||||
tagged.addAll(aggEvent.getEventIDsWithTags());
|
||||
hashHits.addAll(aggEvent.getEventIDsWithHashHits());
|
||||
public EventStripe(EventCluster cluster) {
|
||||
spans.add(cluster.getRange());
|
||||
spanMap.put(cluster.getRange(), cluster);
|
||||
type = cluster.getEventType();
|
||||
description = cluster.getDescription();
|
||||
lod = cluster.getDescriptionLOD();
|
||||
eventIDs.addAll(cluster.getEventIDs());
|
||||
tagged.addAll(cluster.getEventIDsWithTags());
|
||||
hashHits.addAll(cluster.getEventIDsWithHashHits());
|
||||
}
|
||||
|
||||
private EventStripe(EventStripe u, EventStripe v) {
|
||||
|
@ -0,0 +1,452 @@
|
||||
/*
|
||||
* 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 java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.BorderWidths;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
|
||||
import static javafx.scene.layout.Region.USE_PREF_SIZE;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Interval;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.LoggedTask;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.TextFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
|
||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||
|
||||
public abstract class AbstractDetailViewNode< T extends EventBundle, S extends AbstractDetailViewNode<T, S>> extends StackPane implements DetailViewNode<AbstractDetailViewNode<T, S>> {
|
||||
|
||||
static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
|
||||
static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
|
||||
static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
|
||||
static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
|
||||
static final CornerRadii CORNER_RADII = new CornerRadii(3);
|
||||
Map<EventType, DropShadow> dropShadowMap = new HashMap<>();
|
||||
|
||||
static void configureLODButton(Button b) {
|
||||
b.setMinSize(16, 16);
|
||||
b.setMaxSize(16, 16);
|
||||
b.setPrefSize(16, 16);
|
||||
show(b, false);
|
||||
}
|
||||
/**
|
||||
* the border to apply when this node is 'selected'
|
||||
*/
|
||||
static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
|
||||
final Color evtColor;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<S> getSubNodes() {
|
||||
return subNodePane.getChildrenUnmodifiable().stream()
|
||||
.map(t -> (S) t)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate selection
|
||||
*
|
||||
* @param applied true to apply the selection 'effect', false to remove it
|
||||
*/
|
||||
@Override
|
||||
public void applySelectionEffect(boolean applied) {
|
||||
Platform.runLater(() -> {
|
||||
if (applied) {
|
||||
setBorder(selectionBorder);
|
||||
} else {
|
||||
setBorder(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void show(Node b, boolean show) {
|
||||
b.setVisible(show);
|
||||
b.setManaged(show);
|
||||
}
|
||||
final ImageView hashIV = new ImageView(HASH_PIN);
|
||||
final ImageView tagIV = new ImageView(TAG);
|
||||
private final S parentNode;
|
||||
DescriptionVisibility descrVis;
|
||||
|
||||
/**
|
||||
* Pane that contains AggregateEventNodes of any 'subevents' if they are
|
||||
* displayed
|
||||
*
|
||||
* //TODO: move more of the control of subnodes/events here and out of
|
||||
* EventDetail Chart
|
||||
*/
|
||||
final Pane subNodePane = new Pane();
|
||||
|
||||
/**
|
||||
* The ImageView used to show the icon for this node's event's type
|
||||
*/
|
||||
private final ImageView eventTypeImageView = new ImageView();
|
||||
|
||||
/**
|
||||
* The label used to display this node's event's description
|
||||
*/
|
||||
final Label descrLabel = new Label();
|
||||
|
||||
/**
|
||||
* The label used to display this node's event count
|
||||
*/
|
||||
final Label countLabel = new Label();
|
||||
|
||||
private final T eventBundle;
|
||||
private final EventDetailChart chart;
|
||||
final SleuthkitCase sleuthkitCase;
|
||||
final FilteredEventsModel eventsModel;
|
||||
|
||||
final Button plusButton = new Button(null, new ImageView(PLUS)) {
|
||||
{
|
||||
configureLODButton(this);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
final Button minusButton = new Button(null, new ImageView(MINUS)) {
|
||||
{
|
||||
configureLODButton(this);
|
||||
}
|
||||
};
|
||||
SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
|
||||
final HBox header;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param showControls the value of par
|
||||
*/
|
||||
void showDescriptionLoDControls(final boolean showControls) {
|
||||
DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
|
||||
eventType -> new DropShadow(10, eventType.getColor()));
|
||||
getSpanFillNode().setEffect(showControls ? dropShadow : null);
|
||||
show(minusButton, showControls);
|
||||
show(plusButton, showControls);
|
||||
}
|
||||
final Region spacer = new Region();
|
||||
|
||||
RootFilter getSubClusterFilter() {
|
||||
RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf();
|
||||
//make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters
|
||||
combinedFilter.getSubFilters().addAll(new TextFilter(getEventBundle().getDescription()),
|
||||
new TypeFilter(getEventType()));
|
||||
return combinedFilter;
|
||||
}
|
||||
|
||||
abstract Collection<T> makeBundlesFromClusters(List<EventCluster> eventClusters);
|
||||
|
||||
abstract void showSpans(final boolean showSpans);
|
||||
|
||||
public AbstractDetailViewNode(EventDetailChart chart, T bundle, S parentEventNode) {
|
||||
this.eventBundle = bundle;
|
||||
this.parentNode = parentEventNode;
|
||||
this.chart = chart;
|
||||
descLOD.set(bundle.getDescriptionLOD());
|
||||
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
|
||||
eventsModel = chart.getController().getEventsModel();
|
||||
|
||||
if (eventBundle.getEventIDsWithHashHits().isEmpty()) {
|
||||
show(hashIV, false);
|
||||
}
|
||||
if (eventBundle.getEventIDsWithTags().isEmpty()) {
|
||||
show(tagIV, false);
|
||||
}
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, /*spacer,*/ minusButton, plusButton);
|
||||
|
||||
header.setMinWidth(USE_PREF_SIZE);
|
||||
header.setPadding(new Insets(2, 5, 2, 5));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
//setup description label
|
||||
evtColor = getEventType().getColor();
|
||||
eventTypeImageView.setImage(getEventType().getFXImage());
|
||||
descrLabel.setGraphic(eventTypeImageView);
|
||||
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||
descrLabel.setMouseTransparent(true);
|
||||
|
||||
//set up subnode pane sizing contraints
|
||||
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
|
||||
subNodePane.setMinHeight(USE_PREF_SIZE);
|
||||
subNodePane.setMinWidth(USE_PREF_SIZE);
|
||||
subNodePane.setMaxHeight(USE_PREF_SIZE);
|
||||
subNodePane.setMaxWidth(USE_PREF_SIZE);
|
||||
subNodePane.setPickOnBounds(false);
|
||||
|
||||
setAlignment(Pos.TOP_LEFT);
|
||||
setMinHeight(24);
|
||||
|
||||
setPrefHeight(USE_COMPUTED_SIZE);
|
||||
setMaxHeight(USE_PREF_SIZE);
|
||||
setOnMouseClicked(new EventMouseHandler());
|
||||
|
||||
//set up mouse hover effect and tooltip
|
||||
setOnMouseEntered((MouseEvent e) -> {
|
||||
//defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
|
||||
installTooltip();
|
||||
showDescriptionLoDControls(true);
|
||||
toFront();
|
||||
});
|
||||
|
||||
setOnMouseExited((MouseEvent e) -> {
|
||||
showDescriptionLoDControls(false);
|
||||
});
|
||||
setCursor(Cursor.HAND);
|
||||
|
||||
plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
|
||||
minusButton.disableProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD()));
|
||||
|
||||
plusButton.setOnMouseClicked(e -> {
|
||||
final DescriptionLOD next = descLOD.get().next();
|
||||
if (next != null) {
|
||||
loadSubClusters(next);
|
||||
descLOD.set(next);
|
||||
}
|
||||
});
|
||||
minusButton.setOnMouseClicked(e -> {
|
||||
final DescriptionLOD previous = descLOD.get().previous();
|
||||
if (previous != null) {
|
||||
loadSubClusters(previous);
|
||||
descLOD.set(previous);
|
||||
}
|
||||
});
|
||||
|
||||
setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
|
||||
setLayoutX(getChart().getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param w the maximum width the description label should have
|
||||
*/
|
||||
@Override
|
||||
public void setDescriptionWidth(double w) {
|
||||
getDescrLabel().setMaxWidth(w);
|
||||
}
|
||||
|
||||
abstract void installTooltip();
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate highlighted nodes
|
||||
*
|
||||
* @param applied true to apply the highlight 'effect', false to remove it
|
||||
*/
|
||||
@Override
|
||||
public synchronized void applyHighlightEffect(boolean applied) {
|
||||
if (applied) {
|
||||
getDescrLabel().setStyle("-fx-font-weight: bold;"); // NON-NLS
|
||||
getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)));
|
||||
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
|
||||
} else {
|
||||
getDescrLabel().setStyle("-fx-font-weight: normal;"); // NON-NLS
|
||||
getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
}
|
||||
}
|
||||
|
||||
String getDisplayedDescription() {
|
||||
return getDescrLabel().getText();
|
||||
}
|
||||
|
||||
abstract Region getSpanFillNode();
|
||||
|
||||
Button getPlusButton() {
|
||||
return plusButton;
|
||||
}
|
||||
|
||||
Button getMinusButton() {
|
||||
return minusButton;
|
||||
}
|
||||
|
||||
public final Label getDescrLabel() {
|
||||
return descrLabel;
|
||||
}
|
||||
|
||||
public final Label getCountLabel() {
|
||||
return countLabel;
|
||||
}
|
||||
|
||||
public S getParentNode() {
|
||||
return parentNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final T getEventBundle() {
|
||||
return eventBundle;
|
||||
}
|
||||
|
||||
public final EventDetailChart getChart() {
|
||||
return chart;
|
||||
}
|
||||
|
||||
public DescriptionLOD getDescLOD() {
|
||||
return descLOD.get();
|
||||
}
|
||||
private static final Logger LOGGER = Logger.getLogger(AbstractDetailViewNode.class.getName());
|
||||
|
||||
/**
|
||||
* loads sub-clusters at the given Description LOD
|
||||
*
|
||||
* @param newDescriptionLOD
|
||||
*/
|
||||
final synchronized void loadSubClusters(DescriptionLOD newDescriptionLOD) {
|
||||
subNodePane.getChildren().clear();
|
||||
if (newDescriptionLOD == getEventBundle().getDescriptionLOD()) {
|
||||
showSpans(true);
|
||||
getChart().setRequiresLayout(true);
|
||||
getChart().requestChartLayout();
|
||||
} else {
|
||||
showSpans(false);
|
||||
RootFilter combinedFilter = getSubClusterFilter();
|
||||
|
||||
//make a new end inclusive span (to 'filter' with)
|
||||
final Interval span = new Interval(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000);
|
||||
|
||||
//make a task to load the subnodes
|
||||
LoggedTask<List<S>> loggedTask = new LoggedTask<List<S>>(
|
||||
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
|
||||
|
||||
@Override
|
||||
protected List<S> call() throws Exception {
|
||||
//query for the sub-clusters
|
||||
List<EventCluster> aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span,
|
||||
eventsModel.eventTypeZoomProperty().get(),
|
||||
combinedFilter,
|
||||
newDescriptionLOD));
|
||||
|
||||
return makeBundlesFromClusters(aggregatedEvents).stream()
|
||||
.map(aggEvent -> {
|
||||
return getNodeForCluser(aggEvent);
|
||||
}).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
try {
|
||||
getChart().setCursor(Cursor.WAIT);
|
||||
//assign subNodes and request chart layout
|
||||
subNodePane.getChildren().setAll(get());
|
||||
setDescriptionVisibility(descrVis);
|
||||
getChart().setRequiresLayout(true);
|
||||
getChart().requestChartLayout();
|
||||
getChart().setCursor(null);
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//start task
|
||||
getChart().getController().monitorTask(loggedTask);
|
||||
}
|
||||
}
|
||||
|
||||
final double getLayoutXCompensation() {
|
||||
return (getParentNode() != null ? getParentNode().getLayoutXCompensation() : 0)
|
||||
+ getBoundsInParent().getMinX();
|
||||
}
|
||||
|
||||
@Override
|
||||
final public void setDescriptionVisibility(DescriptionVisibility descrVis) {
|
||||
this.descrVis = descrVis;
|
||||
final int size = getEventBundle().getEventIDs().size();
|
||||
|
||||
switch (descrVis) {
|
||||
case COUNT_ONLY:
|
||||
descrLabel.setText("");
|
||||
countLabel.setText(String.valueOf(size));
|
||||
break;
|
||||
case HIDDEN:
|
||||
countLabel.setText("");
|
||||
descrLabel.setText("");
|
||||
break;
|
||||
default:
|
||||
case SHOWN:
|
||||
String description = getEventBundle().getDescription();
|
||||
description = getParentNode() != null
|
||||
? " ..." + StringUtils.substringAfter(description, getParentNode().getDescription())
|
||||
: description;
|
||||
descrLabel.setText(description);
|
||||
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* event handler used for mouse events on {@link AggregateEventNode}s
|
||||
*/
|
||||
private class EventMouseHandler implements EventHandler<MouseEvent> {
|
||||
|
||||
@Override
|
||||
public void handle(MouseEvent t) {
|
||||
|
||||
if (t.getButton() == MouseButton.PRIMARY) {
|
||||
t.consume();
|
||||
if (t.isShiftDown()) {
|
||||
if (chart.selectedNodes.contains(AbstractDetailViewNode.this) == false) {
|
||||
chart.selectedNodes.add(AbstractDetailViewNode.this);
|
||||
}
|
||||
} else if (t.isShortcutDown()) {
|
||||
chart.selectedNodes.removeAll(AbstractDetailViewNode.this);
|
||||
} else if (t.getClickCount() > 1) {
|
||||
final DescriptionLOD next = descLOD.get().next();
|
||||
if (next != null) {
|
||||
loadSubClusters(next);
|
||||
descLOD.set(next);
|
||||
}
|
||||
} else {
|
||||
chart.selectedNodes.setAll(AbstractDetailViewNode.this);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract S getNodeForCluser(T cluster);
|
||||
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
@ -15,24 +16,14 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
*/
|
||||
public interface DetailViewNode<S extends DetailViewNode<S>> {
|
||||
|
||||
public long getStartMillis();
|
||||
|
||||
public long getEndMillis();
|
||||
|
||||
public void setDescriptionVisibility(DescriptionVisibility get);
|
||||
|
||||
public List<S> getSubNodes();
|
||||
public List<? extends S> getSubNodes();
|
||||
|
||||
public void setSpanWidths(List<Double> spanWidths);
|
||||
|
||||
public void setDescriptionWidth(double max);
|
||||
|
||||
public EventType getEventType();
|
||||
|
||||
public Set<Long> getEventIDs();
|
||||
|
||||
public String getDescription();
|
||||
|
||||
public EventBundle getEventBundle();
|
||||
|
||||
/**
|
||||
@ -43,4 +34,34 @@ public interface DetailViewNode<S extends DetailViewNode<S>> {
|
||||
void applyHighlightEffect(boolean applied);
|
||||
|
||||
public void applySelectionEffect(boolean applied);
|
||||
|
||||
default String getDescription() {
|
||||
return getEventBundle().getDescription();
|
||||
}
|
||||
|
||||
default EventType getEventType() {
|
||||
return getEventBundle().getEventType();
|
||||
}
|
||||
|
||||
default Set<Long> getEventIDs() {
|
||||
return getEventBundle().getEventIDs();
|
||||
}
|
||||
|
||||
default public long getStartMillis() {
|
||||
return getEventBundle().getStartMillis();
|
||||
}
|
||||
|
||||
default long getEndMillis() {
|
||||
return getEventBundle().getEndMillis();
|
||||
}
|
||||
|
||||
|
||||
|
||||
static class StartTimeComparator implements Comparator<DetailViewNode<?>> {
|
||||
|
||||
@Override
|
||||
public int compare(DetailViewNode<?> o1, DetailViewNode<?> o2) {
|
||||
return Long.compare(o1.getStartMillis(), o2.getStartMillis());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,6 @@ import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
|
||||
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
@ -101,7 +100,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
|
||||
* TODO: refactor common code out of this class and CountsChartPane into
|
||||
* {@link AbstractVisualization}
|
||||
*/
|
||||
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, EventBundle, EventDetailChart> {
|
||||
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, DetailViewNode<?>, EventDetailChart> {
|
||||
|
||||
private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName());
|
||||
|
||||
@ -358,9 +357,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applySelectionEffect(EventBundle c1, Boolean selected) {
|
||||
protected void applySelectionEffect(DetailViewNode<?> c1, Boolean selected) {
|
||||
chart.applySelectionEffect(c1, selected);
|
||||
|
||||
}
|
||||
|
||||
private class DetailViewSettingsPane extends HBox {
|
||||
|
@ -18,120 +18,39 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.BorderWidths;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Interval;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
|
||||
import org.sleuthkit.autopsy.coreutils.LoggedTask;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.TextFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
|
||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
|
||||
/**
|
||||
* Represents an {@link EventCluster} in a {@link EventDetailChart}.
|
||||
*/
|
||||
public class EventClusterNode extends StackPane implements DetailViewNode<EventClusterNode> {
|
||||
public class EventClusterNode extends AbstractDetailViewNode<EventCluster, EventClusterNode> {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
|
||||
|
||||
private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
|
||||
private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
|
||||
private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
|
||||
private final static Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
|
||||
|
||||
private static final CornerRadii CORNER_RADII = new CornerRadii(3);
|
||||
|
||||
/**
|
||||
* the border to apply when this node is 'selected'
|
||||
*/
|
||||
private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
|
||||
|
||||
/**
|
||||
* The event this AggregateEventNode represents visually
|
||||
*/
|
||||
private EventCluster eventCluster;
|
||||
|
||||
private final EventClusterNode parentEventNode;
|
||||
|
||||
/**
|
||||
* the region that represents the time span of this node's event
|
||||
*/
|
||||
private final Region spanRegion = new Region();
|
||||
|
||||
/**
|
||||
* The label used to display this node's event's description
|
||||
*/
|
||||
private final Label descrLabel = new Label();
|
||||
|
||||
/**
|
||||
* The label used to display this node's event count
|
||||
*/
|
||||
private final Label countLabel = new Label();
|
||||
|
||||
/**
|
||||
* The IamgeView used to show the icon for this node's event's type
|
||||
*/
|
||||
private final ImageView eventTypeImageView = new ImageView();
|
||||
|
||||
/**
|
||||
* Pane that contains AggregateEventNodes of any 'subevents' if they are
|
||||
* displayed
|
||||
*
|
||||
* //TODO: move more of the control of subnodes/events here and out of
|
||||
* EventDetail Chart
|
||||
*/
|
||||
private final Pane subNodePane = new Pane();
|
||||
|
||||
/**
|
||||
* the context menu that with the slider that controls subnode/event display
|
||||
*
|
||||
@ -140,153 +59,34 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
|
||||
*/
|
||||
private final SimpleObjectProperty<ContextMenu> contextMenu = new SimpleObjectProperty<>();
|
||||
|
||||
/**
|
||||
* the Background used to fill the spanRegion, this varies epending on the
|
||||
* selected/highlighted state of this node in its parent EventDetailChart
|
||||
*/
|
||||
private Background spanFill;
|
||||
|
||||
private final Button plusButton = new Button(null, new ImageView(PLUS)) {
|
||||
{
|
||||
configureLODButton(this);
|
||||
}
|
||||
};
|
||||
private final Button minusButton = new Button(null, new ImageView(MINUS)) {
|
||||
{
|
||||
configureLODButton(this);
|
||||
}
|
||||
};
|
||||
|
||||
private static void configureLODButton(Button b) {
|
||||
b.setMinSize(16, 16);
|
||||
b.setMaxSize(16, 16);
|
||||
b.setPrefSize(16, 16);
|
||||
b.setVisible(false);
|
||||
b.setManaged(false);
|
||||
}
|
||||
|
||||
private final EventDetailChart chart;
|
||||
|
||||
private SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
|
||||
private DescriptionVisibility descrVis;
|
||||
private final SleuthkitCase sleuthkitCase;
|
||||
private final FilteredEventsModel eventsModel;
|
||||
|
||||
private Tooltip tooltip;
|
||||
private final ImageView hashIV = new ImageView(HASH_PIN);
|
||||
private final ImageView tagIV = new ImageView(TAG);
|
||||
|
||||
public EventClusterNode(final EventCluster aggEvent, EventClusterNode parentEventNode, EventDetailChart chart) {
|
||||
this.eventCluster = aggEvent;
|
||||
descLOD.set(aggEvent.getDescriptionLOD());
|
||||
this.parentEventNode = parentEventNode;
|
||||
this.chart = chart;
|
||||
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
|
||||
eventsModel = chart.getController().getEventsModel();
|
||||
public EventClusterNode(final EventCluster eventCluster, EventClusterNode parentEventNode, EventDetailChart chart) {
|
||||
super(chart, eventCluster, parentEventNode);
|
||||
minWidthProperty().bind(spanRegion.widthProperty());
|
||||
header.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
|
||||
final Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
|
||||
final HBox hBox = new HBox(descrLabel, countLabel, region, hashIV, tagIV, minusButton, plusButton);
|
||||
if (aggEvent.getEventIDsWithHashHits().isEmpty()) {
|
||||
hashIV.setManaged(false);
|
||||
hashIV.setVisible(false);
|
||||
}
|
||||
if (aggEvent.getEventIDsWithTags().isEmpty()) {
|
||||
tagIV.setManaged(false);
|
||||
tagIV.setVisible(false);
|
||||
}
|
||||
hBox.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
hBox.setMinWidth(USE_PREF_SIZE);
|
||||
hBox.setPadding(new Insets(2, 5, 2, 5));
|
||||
hBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null);
|
||||
final BorderPane borderPane = new BorderPane(subNodePane, header, null, null, null);
|
||||
BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT);
|
||||
borderPane.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
|
||||
getChildren().addAll(spanRegion, borderPane);
|
||||
|
||||
setAlignment(Pos.TOP_LEFT);
|
||||
setMinHeight(24);
|
||||
minWidthProperty().bind(spanRegion.widthProperty());
|
||||
setPrefHeight(USE_COMPUTED_SIZE);
|
||||
setMaxHeight(USE_PREF_SIZE);
|
||||
|
||||
//set up subnode pane sizing contraints
|
||||
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
|
||||
subNodePane.setMinHeight(USE_PREF_SIZE);
|
||||
subNodePane.setMinWidth(USE_PREF_SIZE);
|
||||
subNodePane.setMaxHeight(USE_PREF_SIZE);
|
||||
subNodePane.setMaxWidth(USE_PREF_SIZE);
|
||||
subNodePane.setPickOnBounds(false);
|
||||
|
||||
//setup description label
|
||||
eventTypeImageView.setImage(aggEvent.getEventType().getFXImage());
|
||||
descrLabel.setGraphic(eventTypeImageView);
|
||||
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||
|
||||
descrLabel.setMouseTransparent(true);
|
||||
setDescriptionVisibility(chart.getDescrVisibility().get());
|
||||
|
||||
//setup backgrounds
|
||||
final Color evtColor = aggEvent.getEventType().getColor();
|
||||
spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
|
||||
setBackground(spanFill);
|
||||
setCursor(Cursor.HAND);
|
||||
spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS
|
||||
spanRegion.setBackground(spanFill);
|
||||
spanRegion.setBackground(getBackground());
|
||||
|
||||
//set up mouse hover effect and tooltip
|
||||
setOnMouseEntered((MouseEvent e) -> {
|
||||
//defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
|
||||
installTooltip();
|
||||
spanRegion.setEffect(new DropShadow(10, evtColor));
|
||||
minusButton.setVisible(true);
|
||||
plusButton.setVisible(true);
|
||||
minusButton.setManaged(true);
|
||||
plusButton.setManaged(true);
|
||||
toFront();
|
||||
});
|
||||
|
||||
setOnMouseExited((MouseEvent e) -> {
|
||||
spanRegion.setEffect(null);
|
||||
minusButton.setVisible(false);
|
||||
plusButton.setVisible(false);
|
||||
minusButton.setManaged(false);
|
||||
plusButton.setManaged(false);
|
||||
});
|
||||
|
||||
setOnMouseClicked(new EventMouseHandler());
|
||||
|
||||
plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
|
||||
minusButton.disableProperty().bind(descLOD.isEqualTo(aggEvent.getDescriptionLOD()));
|
||||
|
||||
plusButton.setOnMouseClicked(e -> {
|
||||
final DescriptionLOD next = descLOD.get().next();
|
||||
if (next != null) {
|
||||
loadSubClusters(next);
|
||||
descLOD.set(next);
|
||||
}
|
||||
});
|
||||
minusButton.setOnMouseClicked(e -> {
|
||||
final DescriptionLOD previous = descLOD.get().previous();
|
||||
if (previous != null) {
|
||||
loadSubClusters(previous);
|
||||
descLOD.set(previous);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
synchronized private void installTooltip() {
|
||||
@Override
|
||||
synchronized void installTooltip() {
|
||||
//TODO: all this work should probably go on a background thread...
|
||||
if (tooltip == null) {
|
||||
HashMap<String, Long> hashSetCounts = new HashMap<>();
|
||||
if (!eventCluster.getEventIDsWithHashHits().isEmpty()) {
|
||||
if (!getEventCluster().getEventIDsWithHashHits().isEmpty()) {
|
||||
hashSetCounts = new HashMap<>();
|
||||
try {
|
||||
for (TimeLineEvent tle : eventsModel.getEventsById(eventCluster.getEventIDsWithHashHits())) {
|
||||
for (TimeLineEvent tle : eventsModel.getEventsById(getEventCluster().getEventIDsWithHashHits())) {
|
||||
Set<String> hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames();
|
||||
for (String hashSetName : hashSetNames) {
|
||||
hashSetCounts.merge(hashSetName, 1L, Long::sum);
|
||||
@ -298,8 +98,8 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
|
||||
}
|
||||
|
||||
Map<String, Long> tagCounts = new HashMap<>();
|
||||
if (!eventCluster.getEventIDsWithTags().isEmpty()) {
|
||||
tagCounts.putAll(eventsModel.getTagCountsByTagName(eventCluster.getEventIDsWithTags()));
|
||||
if (!getEventCluster().getEventIDsWithTags().isEmpty()) {
|
||||
tagCounts.putAll(eventsModel.getTagCountsByTagName(getEventCluster().getEventIDsWithTags()));
|
||||
|
||||
}
|
||||
|
||||
@ -312,9 +112,9 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
|
||||
|
||||
tooltip = new Tooltip(
|
||||
NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text",
|
||||
getEvent().getEventIDs().size(), getEvent().getEventType(), getEvent().getDescription(),
|
||||
getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter()),
|
||||
getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()))
|
||||
getEventCluster().getEventIDs().size(), getEventCluster().getEventType(), getEventCluster().getDescription(),
|
||||
getEventCluster().getSpan().getStart().toString(TimeLineController.getZonedFormatter()),
|
||||
getEventCluster().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()))
|
||||
+ (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString)
|
||||
+ (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString)
|
||||
);
|
||||
@ -322,15 +122,8 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EventClusterNode> getSubNodes() {
|
||||
return subNodePane.getChildrenUnmodifiable().stream()
|
||||
.map(EventClusterNode.class::cast)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
synchronized public EventCluster getEvent() {
|
||||
return eventCluster;
|
||||
synchronized public EventCluster getEventCluster() {
|
||||
return getEventBundle();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -351,93 +144,9 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param w the maximum width the description label should have
|
||||
*/
|
||||
@Override
|
||||
public void setDescriptionWidth(double w) {
|
||||
descrLabel.setMaxWidth(w);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param descrVis the level of description that should be displayed
|
||||
*/
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
@Override
|
||||
public final synchronized void setDescriptionVisibility(DescriptionVisibility descrVis) {
|
||||
this.descrVis = descrVis;
|
||||
final int size = eventCluster.getEventIDs().size();
|
||||
|
||||
switch (descrVis) {
|
||||
case COUNT_ONLY:
|
||||
descrLabel.setText("");
|
||||
countLabel.setText(String.valueOf(size));
|
||||
break;
|
||||
case HIDDEN:
|
||||
countLabel.setText("");
|
||||
descrLabel.setText("");
|
||||
break;
|
||||
default:
|
||||
case SHOWN:
|
||||
String description = eventCluster.getDescription();
|
||||
description = parentEventNode != null
|
||||
? " ..." + StringUtils.substringAfter(description, parentEventNode.getEvent().getDescription())
|
||||
: description;
|
||||
descrLabel.setText(description);
|
||||
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate selection
|
||||
*
|
||||
* @param applied true to apply the selection 'effect', false to remove it
|
||||
*/
|
||||
@Override
|
||||
public void applySelectionEffect(boolean applied) {
|
||||
Platform.runLater(() -> {
|
||||
if (applied) {
|
||||
setBorder(selectionBorder);
|
||||
} else {
|
||||
setBorder(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return eventCluster.getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate highlighted nodes
|
||||
*
|
||||
* @param applied true to apply the highlight 'effect', false to remove it
|
||||
*/
|
||||
public synchronized void applyHighlightEffect(boolean applied) {
|
||||
|
||||
if (applied) {
|
||||
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
|
||||
spanFill = new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY));
|
||||
spanRegion.setBackground(spanFill);
|
||||
setBackground(new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
|
||||
} else {
|
||||
descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS
|
||||
spanFill = new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
|
||||
spanRegion.setBackground(spanFill);
|
||||
setBackground(new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
}
|
||||
}
|
||||
|
||||
String getDisplayedDescription() {
|
||||
return descrLabel.getText();
|
||||
}
|
||||
|
||||
double getLayoutXCompensation() {
|
||||
return (parentEventNode != null ? parentEventNode.getLayoutXCompensation() : 0)
|
||||
+ getBoundsInParent().getMinX();
|
||||
Region getSpanFillNode() {
|
||||
return spanRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -454,115 +163,19 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
|
||||
this.contextMenu.set(contextMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* loads sub-clusters at the given Description LOD
|
||||
*
|
||||
* @param newDescriptionLOD
|
||||
*/
|
||||
synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) {
|
||||
subNodePane.getChildren().clear();
|
||||
if (newDescriptionLOD == eventCluster.getDescriptionLOD()) {
|
||||
chart.setRequiresLayout(true);
|
||||
chart.requestChartLayout();
|
||||
} else {
|
||||
RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf();
|
||||
//make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters
|
||||
combinedFilter.getSubFilters().addAll(new TextFilter(eventCluster.getDescription()),
|
||||
new TypeFilter(eventCluster.getEventType()));
|
||||
|
||||
//make a new end inclusive span (to 'filter' with)
|
||||
final Interval span = eventCluster.getSpan().withEndMillis(eventCluster.getSpan().getEndMillis() + 1000);
|
||||
|
||||
//make a task to load the subnodes
|
||||
LoggedTask<List<EventClusterNode>> loggedTask = new LoggedTask<List<EventClusterNode>>(
|
||||
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
|
||||
|
||||
@Override
|
||||
protected List<EventClusterNode> call() throws Exception {
|
||||
//query for the sub-clusters
|
||||
List<EventCluster> aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span,
|
||||
eventsModel.eventTypeZoomProperty().get(),
|
||||
combinedFilter,
|
||||
newDescriptionLOD));
|
||||
//for each sub cluster make an AggregateEventNode to visually represent it, and set x-position
|
||||
return aggregatedEvents.stream().map(aggEvent -> {
|
||||
EventClusterNode subNode = new EventClusterNode(aggEvent, EventClusterNode.this, chart);
|
||||
subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())) - getLayoutXCompensation());
|
||||
return subNode;
|
||||
}).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
try {
|
||||
chart.setCursor(Cursor.WAIT);
|
||||
//assign subNodes and request chart layout
|
||||
subNodePane.getChildren().setAll(get());
|
||||
setDescriptionVisibility(descrVis);
|
||||
chart.setRequiresLayout(true);
|
||||
chart.requestChartLayout();
|
||||
chart.setCursor(null);
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//start task
|
||||
chart.getController().monitorTask(loggedTask);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* event handler used for mouse events on {@link EventClusterNode}s
|
||||
*/
|
||||
private class EventMouseHandler implements EventHandler<MouseEvent> {
|
||||
|
||||
@Override
|
||||
public void handle(MouseEvent t) {
|
||||
if (t.getButton() == MouseButton.PRIMARY) {
|
||||
t.consume();
|
||||
if (t.isShiftDown()) {
|
||||
if (chart.selectedBundles.contains(eventCluster) == false) {
|
||||
chart.selectedBundles.add(eventCluster);
|
||||
}
|
||||
} else if (t.isShortcutDown()) {
|
||||
chart.selectedBundles.removeAll(eventCluster);
|
||||
} else if (t.getClickCount() > 1) {
|
||||
final DescriptionLOD next = descLOD.get().next();
|
||||
if (next != null) {
|
||||
loadSubClusters(next);
|
||||
descLOD.set(next);
|
||||
}
|
||||
} else {
|
||||
chart.selectedBundles.setAll(eventCluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
void showSpans(boolean showSpans) {
|
||||
//no-op for now
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStartMillis() {
|
||||
return getEvent().getStartMillis();
|
||||
Collection<EventCluster> makeBundlesFromClusters(List<EventCluster> eventClusters) {
|
||||
return eventClusters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEndMillis() {
|
||||
return getEvent().getStartMillis();
|
||||
EventClusterNode getNodeForCluser(EventCluster cluster) {
|
||||
return new EventClusterNode(cluster, this, getChart());
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventType getEventType() {
|
||||
return getEvent().getEventType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getEventIDs() {
|
||||
return getEvent().getEventIDs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventBundle getEventBundle() {
|
||||
return eventCluster;
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,6 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.scene.shape.StrokeLineCap;
|
||||
import javafx.util.Duration;
|
||||
@ -75,7 +74,6 @@ import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.actions.Back;
|
||||
import org.sleuthkit.autopsy.timeline.actions.Forward;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
@ -175,7 +173,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
||||
@GuardedBy(value = "this")
|
||||
private boolean requiresLayout = true;
|
||||
|
||||
final ObservableList<EventBundle> selectedBundles;
|
||||
final ObservableList<DetailViewNode<?>> selectedNodes;
|
||||
|
||||
/**
|
||||
* list of series of data added to this chart TODO: replace this with a map
|
||||
@ -205,7 +203,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
||||
private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
|
||||
private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true);
|
||||
|
||||
EventDetailChart(DateAxis dateAxis, final Axis<EventCluster> verticalAxis, ObservableList<EventBundle> selectedNodes) {
|
||||
EventDetailChart(DateAxis dateAxis, final Axis<EventCluster> verticalAxis, ObservableList<DetailViewNode<?>> selectedNodes) {
|
||||
super(dateAxis, verticalAxis);
|
||||
dateAxis.setAutoRanging(false);
|
||||
|
||||
@ -284,38 +282,28 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
||||
setOnMouseReleased(dragHandler);
|
||||
setOnMouseDragged(dragHandler);
|
||||
|
||||
// projectionMap.addListener((MapChangeListener.Change<? extends DetailViewNode<?>, ? extends Line> change) -> {
|
||||
// final Line valueRemoved = change.getValueRemoved();
|
||||
// if (valueRemoved != null) {
|
||||
// getChartChildren().removeAll(valueRemoved);
|
||||
// }
|
||||
// final Line valueAdded = change.getValueAdded();
|
||||
// if (valueAdded != null) {
|
||||
// getChartChildren().add(valueAdded);
|
||||
// }
|
||||
// });
|
||||
this.selectedBundles = selectedNodes;
|
||||
this.selectedBundles.addListener((
|
||||
ListChangeListener.Change<? extends EventBundle> c) -> {
|
||||
this.selectedNodes = selectedNodes;
|
||||
this.selectedNodes.addListener((
|
||||
ListChangeListener.Change<? extends DetailViewNode<?>> c) -> {
|
||||
while (c.next()) {
|
||||
c.getRemoved().forEach((EventBundle t) -> {
|
||||
t.getRanges().forEach((Range<Long> t1) -> {
|
||||
Line removeAll = projectionMap.remove(t1);
|
||||
getChartChildren().removeAll(removeAll);
|
||||
c.getRemoved().forEach((DetailViewNode<?> t) -> {
|
||||
t.getEventBundle().getRanges().forEach((Range<Long> t1) -> {
|
||||
Line removedLine = projectionMap.remove(t1);
|
||||
getChartChildren().removeAll(removedLine);
|
||||
});
|
||||
|
||||
});
|
||||
c.getAddedSubList().forEach((EventBundle t) -> {
|
||||
c.getAddedSubList().forEach((DetailViewNode<?> t) -> {
|
||||
|
||||
for (Range<Long> r : t.getRanges()) {
|
||||
for (Range<Long> range : t.getEventBundle().getRanges()) {
|
||||
|
||||
Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(r.lowerEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
|
||||
dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(r.upperEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
|
||||
Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.lowerEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
|
||||
dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.upperEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
|
||||
);
|
||||
line.setStroke(t.getEventType().getColor().deriveColor(0, 1, 1, .5));
|
||||
line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
|
||||
line.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
projectionMap.put(r, line);
|
||||
projectionMap.put(range, line);
|
||||
getChartChildren().add(line);
|
||||
}
|
||||
});
|
||||
@ -353,7 +341,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
||||
clearGuideLine();
|
||||
clearIntervalSelector();
|
||||
|
||||
selectedBundles.clear();
|
||||
selectedNodes.clear();
|
||||
projectionMap.clear();
|
||||
controller.selectEventIDs(Collections.emptyList());
|
||||
});
|
||||
@ -600,38 +588,38 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
||||
* @param nodes
|
||||
* @param minY
|
||||
*/
|
||||
private synchronized <DVRegion extends Region & DetailViewNode<DVRegion>> double layoutNodes(final Collection<DVRegion> nodes, final double minY, final double xOffset) {
|
||||
private synchronized double layoutNodes(final Collection<? extends AbstractDetailViewNode<?, ?>> nodes, final double minY, final double xOffset) {
|
||||
//hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot
|
||||
Map<Integer, Double> maxXatY = new HashMap<>();
|
||||
double localMax = minY;
|
||||
//for each node lay size it and position it in first available slot
|
||||
for (DVRegion n : nodes) {
|
||||
n.setDescriptionVisibility(descrVisibility.get());
|
||||
double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(n.getStartMillis()));
|
||||
for (AbstractDetailViewNode<?, ?> node : nodes) {
|
||||
node.setDescriptionVisibility(descrVisibility.get());
|
||||
double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis()));
|
||||
|
||||
//position of start and end according to range of axis
|
||||
double startX = rawDisplayPosition - xOffset;
|
||||
double layoutNodesResultHeight = 0;
|
||||
|
||||
double span = 0;
|
||||
List<DVRegion> subNodes = n.getSubNodes();
|
||||
List<? extends AbstractDetailViewNode<?, ?>> subNodes = node.getSubNodes();
|
||||
if (subNodes.isEmpty() == false) {
|
||||
subNodes.sort(Comparator.comparing((DVRegion t) -> t.getStartMillis()));
|
||||
subNodes.sort(new DetailViewNode.StartTimeComparator());
|
||||
layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition);
|
||||
}
|
||||
|
||||
if (n instanceof EventClusterNode) {
|
||||
double endX = getXAxis().getDisplayPosition(new DateTime(n.getEndMillis())) - xOffset;
|
||||
if (alternateLayout.get() == false) {
|
||||
double endX = getXAxis().getDisplayPosition(new DateTime(node.getEndMillis())) - xOffset;
|
||||
span = endX - startX;
|
||||
//size timespan border
|
||||
n.setSpanWidths(Arrays.asList(span));
|
||||
node.setSpanWidths(Arrays.asList(span));
|
||||
} else {
|
||||
|
||||
EventStripeNode cn = (EventStripeNode) n;
|
||||
EventStripeNode stripeNode = (EventStripeNode) node;
|
||||
List<Double> spanWidths = new ArrayList<>();
|
||||
double x = getXAxis().getDisplayPosition(new DateTime(cn.getStartMillis()));;
|
||||
double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));;
|
||||
double x2;
|
||||
Iterator<Range<Long>> ranges = cn.getStripe().getRanges().iterator();
|
||||
Iterator<Range<Long>> ranges = stripeNode.getStripe().getRanges().iterator();
|
||||
Range<Long> range = ranges.next();
|
||||
do {
|
||||
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
|
||||
@ -644,25 +632,31 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
||||
double gapSpan = x - x2;
|
||||
span += gapSpan;
|
||||
spanWidths.add(gapSpan);
|
||||
if (ranges.hasNext() == false) {
|
||||
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
|
||||
clusterSpan = x2 - x;
|
||||
span += clusterSpan;
|
||||
spanWidths.add(clusterSpan);
|
||||
}
|
||||
}
|
||||
|
||||
} while (ranges.hasNext());
|
||||
|
||||
cn.setSpanWidths(spanWidths);
|
||||
stripeNode.setSpanWidths(spanWidths);
|
||||
}
|
||||
if (truncateAll.get()) { //if truncate option is selected limit width of description label
|
||||
n.setDescriptionWidth(Math.max(span, truncateWidth.get()));
|
||||
node.setDescriptionWidth(Math.max(span, truncateWidth.get()));
|
||||
} else { //else set it unbounded
|
||||
n.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth());
|
||||
node.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth());
|
||||
}
|
||||
|
||||
n.autosize(); //compute size of tlNode based on constraints and event data
|
||||
node.autosize(); //compute size of tlNode based on constraints and event data
|
||||
|
||||
//get position of right edge of node ( influenced by description label)
|
||||
double xRight = startX + n.getWidth();
|
||||
double xRight = startX + node.getWidth();
|
||||
|
||||
//get the height of the node
|
||||
final double h = layoutNodesResultHeight == 0 ? n.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT;
|
||||
final double h = layoutNodesResultHeight == 0 ? node.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT;
|
||||
//initial test position
|
||||
double yPos = minY;
|
||||
|
||||
@ -701,8 +695,8 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
||||
localMax = Math.max(yPos2, localMax);
|
||||
|
||||
Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0),
|
||||
new KeyValue(n.layoutXProperty(), startX),
|
||||
new KeyValue(n.layoutYProperty(), yPos)));
|
||||
new KeyValue(node.layoutXProperty(), startX),
|
||||
new KeyValue(node.layoutYProperty(), yPos)));
|
||||
|
||||
tm.play();
|
||||
}
|
||||
@ -753,15 +747,6 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
||||
return alternateLayout;
|
||||
}
|
||||
|
||||
private static class StartTimeComparator<T extends Node & DetailViewNode<?>> implements Comparator<T> {
|
||||
|
||||
@Override
|
||||
public int compare(T n1, T n2) {
|
||||
return Long.compare(n1.getStartMillis(), n2.getStartMillis()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private class DetailIntervalSelector extends IntervalSelector<DateTime> {
|
||||
|
||||
public DetailIntervalSelector(double x, double height, Axis<DateTime> axis, TimeLineController controller) {
|
||||
@ -797,12 +782,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
||||
super.requestChartLayout();
|
||||
}
|
||||
|
||||
void applySelectionEffect(EventBundle c1, Boolean selected) {
|
||||
if (alternateLayout.get()) {
|
||||
EventStripe eventStripe = stripeDescMap.get(ImmutablePair.of(c1.getEventType(), c1.getDescription()));
|
||||
stripeNodeMap.get(eventStripe).applySelectionEffect(selected);
|
||||
} else {
|
||||
clusterNodeMap.get(c1).applySelectionEffect(selected);
|
||||
}
|
||||
void applySelectionEffect(DetailViewNode<?> c1, Boolean selected) {
|
||||
c1.applySelectionEffect(selected);
|
||||
}
|
||||
}
|
||||
|
@ -6,235 +6,64 @@
|
||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
import java.util.HashMap;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.BorderWidths;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
|
||||
import static javafx.scene.layout.Region.USE_PREF_SIZE;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Interval;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
|
||||
import org.sleuthkit.autopsy.coreutils.LoggedTask;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
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.eventtype.EventType;
|
||||
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.TextFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
|
||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||
import static org.sleuthkit.autopsy.timeline.ui.detailview.AbstractDetailViewNode.show;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class EventStripeNode extends StackPane implements DetailViewNode<EventStripeNode> {
|
||||
public class EventStripeNode extends AbstractDetailViewNode<EventStripe, EventStripeNode> {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
|
||||
|
||||
private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
|
||||
private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
|
||||
private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
|
||||
private final static Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
|
||||
|
||||
private final Pane subNodePane = new Pane();
|
||||
private final EventStripe eventStripe;
|
||||
private final EventStripeNode parentNode;
|
||||
private final EventDetailChart chart;
|
||||
private SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
|
||||
private final SleuthkitCase sleuthkitCase;
|
||||
private final FilteredEventsModel eventsModel;
|
||||
/**
|
||||
* The label used to display this node's event's description
|
||||
*/
|
||||
private final Label descrLabel = new Label();
|
||||
|
||||
/**
|
||||
* The label used to display this node's event count
|
||||
*/
|
||||
private final Label countLabel = new Label();
|
||||
|
||||
private final ImageView hashIV = new ImageView(HASH_PIN);
|
||||
private final ImageView tagIV = new ImageView(TAG);
|
||||
private final Button plusButton = new Button(null, new ImageView(PLUS)) {
|
||||
{
|
||||
configureLODButton(this);
|
||||
}
|
||||
};
|
||||
private final Button minusButton = new Button(null, new ImageView(MINUS)) {
|
||||
{
|
||||
configureLODButton(this);
|
||||
}
|
||||
};
|
||||
|
||||
private static void configureLODButton(Button b) {
|
||||
b.setMinSize(16, 16);
|
||||
b.setMaxSize(16, 16);
|
||||
b.setPrefSize(16, 16);
|
||||
show(b, false);
|
||||
}
|
||||
|
||||
private static void show(Node b, boolean show) {
|
||||
b.setVisible(show);
|
||||
b.setManaged(show);
|
||||
}
|
||||
private DescriptionVisibility descrVis;
|
||||
private final HBox spansHBox = new HBox();
|
||||
/**
|
||||
* The IamgeView used to show the icon for this node's event's type
|
||||
*/
|
||||
private final ImageView eventTypeImageView = new ImageView();
|
||||
private Background spanFill;
|
||||
private static final CornerRadii CORNER_RADII = new CornerRadii(3);
|
||||
private final HBox rangesHBox = new HBox();
|
||||
|
||||
EventStripeNode(EventStripe eventStripe, EventStripeNode parentNode, EventDetailChart chart) {
|
||||
this.chart = chart;
|
||||
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
|
||||
eventsModel = chart.getController().getEventsModel();
|
||||
|
||||
this.parentNode = parentNode;
|
||||
this.eventStripe = eventStripe;
|
||||
descLOD.set(eventStripe.getDescriptionLOD());
|
||||
|
||||
final Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
|
||||
final HBox header = new HBox(descrLabel, countLabel, hashIV, tagIV, spacer, minusButton, plusButton);
|
||||
if (eventStripe.getEventIDsWithHashHits().isEmpty()) {
|
||||
show(hashIV, false);
|
||||
}
|
||||
if (eventStripe.getEventIDsWithTags().isEmpty()) {
|
||||
show(tagIV, false);
|
||||
}
|
||||
header.setMinWidth(USE_PREF_SIZE);
|
||||
header.setPadding(new Insets(2, 5, 2, 5));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
super(chart, eventStripe, parentNode);
|
||||
minWidthProperty().bind(rangesHBox.widthProperty());
|
||||
final VBox internalVBox = new VBox(header, subNodePane);
|
||||
internalVBox.setAlignment(Pos.CENTER_LEFT);
|
||||
final Color evtColor = eventStripe.getEventType().getColor();
|
||||
|
||||
spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY));
|
||||
for (Range<Long> r : eventStripe.getRanges()) {
|
||||
Region spanRegion = new Region();
|
||||
spanRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS
|
||||
spanRegion.setBackground(spanFill);
|
||||
spansHBox.getChildren().addAll(spanRegion, new Region());
|
||||
for (Range<Long> range : eventStripe.getRanges()) {
|
||||
Region rangeRegion = new Region();
|
||||
rangeRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS
|
||||
rangeRegion.setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
|
||||
rangesHBox.getChildren().addAll(rangeRegion, new Region());
|
||||
}
|
||||
spansHBox.getChildren().remove(spansHBox.getChildren().size() - 1);
|
||||
spansHBox.setMaxWidth(USE_PREF_SIZE);
|
||||
rangesHBox.getChildren().remove(rangesHBox.getChildren().size() - 1);
|
||||
rangesHBox.setMaxWidth(USE_PREF_SIZE);
|
||||
setMaxWidth(USE_PREF_SIZE);
|
||||
getChildren().addAll(spansHBox, internalVBox);
|
||||
setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
setAlignment(Pos.TOP_LEFT);
|
||||
setMinHeight(24);
|
||||
minWidthProperty().bind(spansHBox.widthProperty());
|
||||
setPrefHeight(USE_COMPUTED_SIZE);
|
||||
setMaxHeight(USE_PREF_SIZE);
|
||||
|
||||
//set up subnode pane sizing contraints
|
||||
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
|
||||
subNodePane.setMinHeight(USE_PREF_SIZE);
|
||||
subNodePane.setMinWidth(USE_PREF_SIZE);
|
||||
subNodePane.setMaxHeight(USE_PREF_SIZE);
|
||||
subNodePane.setMaxWidth(USE_PREF_SIZE);
|
||||
subNodePane.setPickOnBounds(false);
|
||||
|
||||
//setup description label
|
||||
eventTypeImageView.setImage(eventStripe.getEventType().getFXImage());
|
||||
descrLabel.setGraphic(eventTypeImageView);
|
||||
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||
|
||||
descrLabel.setMouseTransparent(true);
|
||||
setDescriptionVisibility(chart.getDescrVisibility().get());
|
||||
setOnMouseClicked(new EventMouseHandler());
|
||||
|
||||
//set up mouse hover effect and tooltip
|
||||
setOnMouseEntered((MouseEvent e) -> {
|
||||
//defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
|
||||
// installTooltip();
|
||||
spansHBox.setEffect(new DropShadow(10, evtColor));
|
||||
show(spacer, true);
|
||||
show(minusButton, true);
|
||||
show(plusButton, true);
|
||||
|
||||
toFront();
|
||||
});
|
||||
|
||||
setOnMouseExited((MouseEvent e) -> {
|
||||
spansHBox.setEffect(null);
|
||||
show(spacer, false);
|
||||
show(minusButton, false);
|
||||
show(plusButton, false);
|
||||
|
||||
});
|
||||
|
||||
plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
|
||||
minusButton.disableProperty().bind(descLOD.isEqualTo(eventStripe.getDescriptionLOD()));
|
||||
|
||||
plusButton.setOnMouseClicked(e -> {
|
||||
final DescriptionLOD next = descLOD.get().next();
|
||||
if (next != null) {
|
||||
loadSubClusters(next);
|
||||
descLOD.set(next);
|
||||
}
|
||||
});
|
||||
minusButton.setOnMouseClicked(e -> {
|
||||
final DescriptionLOD previous = descLOD.get().previous();
|
||||
if (previous != null) {
|
||||
loadSubClusters(previous);
|
||||
descLOD.set(previous);
|
||||
}
|
||||
});
|
||||
getChildren().addAll(rangesHBox, internalVBox);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param showControls the value of par
|
||||
*/
|
||||
@Override
|
||||
public long getStartMillis() {
|
||||
return eventStripe.getStartMillis();
|
||||
void showDescriptionLoDControls(final boolean showControls) {
|
||||
super.showDescriptionLoDControls(showControls);
|
||||
show(spacer, showControls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSpanWidths(List<Double> spanWidths) {
|
||||
for (int i = 0; i < spanWidths.size(); i++) {
|
||||
Region spanRegion = (Region) spansHBox.getChildren().get(i);
|
||||
Region spanRegion = (Region) rangesHBox.getChildren().get(i);
|
||||
Double w = spanWidths.get(i);
|
||||
spanRegion.setPrefWidth(w);
|
||||
spanRegion.setMaxWidth(w);
|
||||
@ -242,211 +71,42 @@ public class EventStripeNode extends StackPane implements DetailViewNode<EventSt
|
||||
}
|
||||
}
|
||||
|
||||
public void setDescriptionVisibility(DescriptionVisibility descrVis) {
|
||||
this.descrVis = descrVis;
|
||||
final int size = eventStripe.getEventIDs().size();
|
||||
|
||||
switch (descrVis) {
|
||||
case COUNT_ONLY:
|
||||
descrLabel.setText("");
|
||||
countLabel.setText(String.valueOf(size));
|
||||
break;
|
||||
case HIDDEN:
|
||||
countLabel.setText("");
|
||||
descrLabel.setText("");
|
||||
break;
|
||||
default:
|
||||
case SHOWN:
|
||||
String description = eventStripe.getDescription();
|
||||
description = parentNode != null
|
||||
? " ..." + StringUtils.substringAfter(description, parentNode.getDescription())
|
||||
: description;
|
||||
descrLabel.setText(description);
|
||||
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EventStripe getStripe() {
|
||||
return eventStripe;
|
||||
return getEventBundle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDescriptionWidth(double w) {
|
||||
descrLabel.setMaxWidth(w);
|
||||
HBox getSpanFillNode() {
|
||||
return rangesHBox;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEndMillis() {
|
||||
return eventStripe.getEndMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EventStripeNode> getSubNodes() {
|
||||
return subNodePane.getChildrenUnmodifiable().stream()
|
||||
.map(EventStripeNode.class::cast)
|
||||
.collect(Collectors.toList());
|
||||
Collection<EventStripe> makeBundlesFromClusters(List<EventCluster> eventClusters) {
|
||||
return eventClusters.stream().collect(
|
||||
Collectors.toMap(
|
||||
EventCluster::getDescription, //key
|
||||
EventStripe::new, //value
|
||||
EventStripe::merge)//merge method
|
||||
).values();
|
||||
}
|
||||
|
||||
/**
|
||||
* event handler used for mouse events on {@link AggregateEventNode}s
|
||||
*/
|
||||
private class EventMouseHandler implements EventHandler<MouseEvent> {
|
||||
|
||||
@Override
|
||||
public void handle(MouseEvent t) {
|
||||
if (t.getButton() == MouseButton.PRIMARY) {
|
||||
t.consume();
|
||||
if (t.isShiftDown()) {
|
||||
if (chart.selectedBundles.contains(eventStripe) == false) {
|
||||
chart.selectedBundles.add(eventStripe);
|
||||
}
|
||||
} else if (t.isShortcutDown()) {
|
||||
chart.selectedBundles.removeAll(eventStripe);
|
||||
} else if (t.getClickCount() > 1) {
|
||||
final DescriptionLOD next = descLOD.get().next();
|
||||
if (next != null) {
|
||||
loadSubClusters(next);
|
||||
descLOD.set(next);
|
||||
}
|
||||
} else {
|
||||
chart.selectedBundles.setAll(eventStripe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventType getEventType() {
|
||||
return eventStripe.getEventType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getEventIDs() {
|
||||
return eventStripe.getEventIDs();
|
||||
}
|
||||
private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate selection
|
||||
*
|
||||
* @param applied true to apply the selection 'effect', false to remove it
|
||||
* @param showSpans the value of showSpans
|
||||
*/
|
||||
@Override
|
||||
public void applySelectionEffect(boolean applied) {
|
||||
Platform.runLater(() -> {
|
||||
if (applied) {
|
||||
setBorder(selectionBorder);
|
||||
} else {
|
||||
setBorder(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate highlighted nodes
|
||||
*
|
||||
* @param applied true to apply the highlight 'effect', false to remove it
|
||||
*/
|
||||
@Override
|
||||
public synchronized void applyHighlightEffect(boolean applied) {
|
||||
if (applied) {
|
||||
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
|
||||
spanFill = new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY));
|
||||
spansHBox.setBackground(spanFill);
|
||||
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
|
||||
} else {
|
||||
descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS
|
||||
spanFill = new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
|
||||
spansHBox.setBackground(spanFill);
|
||||
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
}
|
||||
void showSpans(final boolean showSpans) {
|
||||
rangesHBox.setVisible(showSpans);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return eventStripe.getDescription();
|
||||
void installTooltip() {
|
||||
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventBundle getEventBundle() {
|
||||
return getStripe();
|
||||
EventStripeNode getNodeForCluser(EventStripe cluster) {
|
||||
return new EventStripeNode(cluster, this, getChart());
|
||||
}
|
||||
|
||||
/**
|
||||
* loads sub-clusters at the given Description LOD
|
||||
*
|
||||
* @param newDescriptionLOD
|
||||
*/
|
||||
synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) {
|
||||
subNodePane.getChildren().clear();
|
||||
|
||||
if (newDescriptionLOD == eventStripe.getDescriptionLOD()) {
|
||||
spansHBox.setVisible(true);
|
||||
chart.setRequiresLayout(true);
|
||||
chart.requestChartLayout();
|
||||
} else {
|
||||
spansHBox.setVisible(false);
|
||||
RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf();
|
||||
//make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters
|
||||
combinedFilter.getSubFilters().addAll(new TextFilter(eventStripe.getDescription()),
|
||||
new TypeFilter(eventStripe.getEventType()));
|
||||
|
||||
//make a new end inclusive span (to 'filter' with)
|
||||
final Interval span = new Interval(eventStripe.getStartMillis(), eventStripe.getEndMillis() + 1000);
|
||||
|
||||
//make a task to load the subnodes
|
||||
LoggedTask<List<EventStripeNode>> loggedTask = new LoggedTask<List<EventStripeNode>>(
|
||||
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
|
||||
|
||||
@Override
|
||||
protected List<EventStripeNode> call() throws Exception {
|
||||
//query for the sub-clusters
|
||||
List<EventCluster> aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span,
|
||||
eventsModel.eventTypeZoomProperty().get(),
|
||||
combinedFilter,
|
||||
newDescriptionLOD));
|
||||
//for each sub cluster make an AggregateEventNode to visually represent it, and set x-position
|
||||
HashMap<String, EventStripe> stripeDescMap = new HashMap<>();
|
||||
for (EventCluster subCluster : aggregatedEvents) {
|
||||
stripeDescMap.merge(subCluster.getDescription(),
|
||||
new EventStripe(subCluster),
|
||||
(EventStripe u, EventStripe v) -> {
|
||||
return EventStripe.merge(u, v);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return stripeDescMap.values().stream().map(subStripe -> {
|
||||
EventStripeNode subNode = new EventStripeNode(subStripe, EventStripeNode.this, chart);
|
||||
subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(subStripe.getStartMillis())) - getLayoutXCompensation());
|
||||
return subNode;
|
||||
}).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
try {
|
||||
chart.setCursor(Cursor.WAIT);
|
||||
//assign subNodes and request chart layout
|
||||
subNodePane.getChildren().setAll(get());
|
||||
setDescriptionVisibility(descrVis);
|
||||
chart.setRequiresLayout(true);
|
||||
chart.requestChartLayout();
|
||||
chart.setCursor(null);
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//start task
|
||||
chart.getController().monitorTask(loggedTask);
|
||||
}
|
||||
}
|
||||
|
||||
double getLayoutXCompensation() {
|
||||
return (parentNode != null ? parentNode.getLayoutXCompensation() : 0)
|
||||
+ getBoundsInParent().getMinX();
|
||||
}
|
||||
}
|
||||
|
@ -41,9 +41,9 @@ import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineView;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewNode;
|
||||
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
|
||||
|
||||
/**
|
||||
@ -91,8 +91,8 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
||||
});
|
||||
detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
|
||||
eventsTree.getSelectionModel().clearSelection();
|
||||
detailViewPane.getSelectedNodes().forEach((EventBundle t) -> {
|
||||
eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t));
|
||||
detailViewPane.getSelectedNodes().forEach((DetailViewNode<?> t) -> {
|
||||
eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEventBundle()));
|
||||
});
|
||||
});
|
||||
|
||||
@ -131,7 +131,7 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
||||
sortByBox.getItems().setAll(Arrays.asList(TreeComparator.Description, TreeComparator.Count));
|
||||
sortByBox.getSelectionModel().select(TreeComparator.Description);
|
||||
sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> {
|
||||
((RootItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem());
|
||||
((NavTreeItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem());
|
||||
});
|
||||
eventsTree.setShowRoot(false);
|
||||
eventsTree.setCellFactory((TreeView<NavTreeNode> p) -> new EventTreeCell());
|
||||
|
@ -20,8 +20,8 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
|
||||
|
||||
import java.util.Comparator;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
|
||||
/**
|
||||
* A node in the nav tree. Manages inserts and resorts. Has parents and
|
||||
|
Loading…
x
Reference in New Issue
Block a user