diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index 9f5cc5428c..7533c486e1 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -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 idsUnion = Sets.union(aggEvent1.getEventIDs(), ag2.getEventIDs()); - Sets.SetView hashHitsUnion = Sets.union(aggEvent1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits()); - Sets.SetView taggedUnion = Sets.union(aggEvent1.getEventIDsWithTags(), ag2.getEventIDsWithTags()); + Sets.SetView idsUnion = Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs()); + Sets.SetView hashHitsUnion = Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits()); + Sets.SetView 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 getRange() { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index 9bd45ce278..e88e8ff623 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -60,15 +60,15 @@ public final class EventStripe implements EventBundle { */ private final Set 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) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java new file mode 100644 index 0000000000..743e49b476 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java @@ -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> extends StackPane implements DetailViewNode> { + + 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 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 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 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 makeBundlesFromClusters(List 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> loggedTask = new LoggedTask>( + NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { + + @Override + protected List call() throws Exception { + //query for the sub-clusters + List 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 { + + @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); + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java index efd3a47586..5ae2ab2e0b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java @@ -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> { - public long getStartMillis(); - - public long getEndMillis(); - public void setDescriptionVisibility(DescriptionVisibility get); - public List getSubNodes(); + public List getSubNodes(); public void setSpanWidths(List spanWidths); public void setDescriptionWidth(double max); - public EventType getEventType(); - - public Set getEventIDs(); - - public String getDescription(); - public EventBundle getEventBundle(); /** @@ -43,4 +34,34 @@ public interface DetailViewNode> { void applyHighlightEffect(boolean applied); public void applySelectionEffect(boolean applied); + + default String getDescription() { + return getEventBundle().getDescription(); + } + + default EventType getEventType() { + return getEventBundle().getEventType(); + } + + default Set getEventIDs() { + return getEventBundle().getEventIDs(); + } + + default public long getStartMillis() { + return getEventBundle().getStartMillis(); + } + + default long getEndMillis() { + return getEventBundle().getEndMillis(); + } + + + + static class StartTimeComparator implements Comparator> { + + @Override + public int compare(DetailViewNode o1, DetailViewNode o2) { + return Long.compare(o1.getStartMillis(), o2.getStartMillis()); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 5f1d32b083..f40948c20b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -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 { +public class DetailViewPane extends AbstractVisualization, EventDetailChart> { private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); @@ -358,9 +357,8 @@ public class DetailViewPane extends AbstractVisualization c1, Boolean selected) { chart.applySelectionEffect(c1, selected); - } private class DetailViewSettingsPane extends HBox { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index 20baf039b1..b07adac60b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -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 { +public class EventClusterNode extends AbstractDetailViewNode { 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 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 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 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 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 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 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 { - 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> loggedTask = new LoggedTask>( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { - - @Override - protected List call() throws Exception { - //query for the sub-clusters - List 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 { - - @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 makeBundlesFromClusters(List 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 getEventIDs() { - return getEvent().getEventIDs(); - } - - @Override - public EventBundle getEventBundle() { - return eventCluster; - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java index 17b38e8e71..5d9bd03eb4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -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 impl @GuardedBy(value = "this") private boolean requiresLayout = true; - final ObservableList selectedBundles; + final ObservableList> 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 impl private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true); - EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList selectedNodes) { + EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { super(dateAxis, verticalAxis); dateAxis.setAutoRanging(false); @@ -284,38 +282,28 @@ public final class EventDetailChart extends XYChart impl setOnMouseReleased(dragHandler); setOnMouseDragged(dragHandler); -// projectionMap.addListener((MapChangeListener.Change, ? 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 c) -> { + this.selectedNodes = selectedNodes; + this.selectedNodes.addListener(( + ListChangeListener.Change> c) -> { while (c.next()) { - c.getRemoved().forEach((EventBundle t) -> { - t.getRanges().forEach((Range t1) -> { - Line removeAll = projectionMap.remove(t1); - getChartChildren().removeAll(removeAll); + c.getRemoved().forEach((DetailViewNode t) -> { + t.getEventBundle().getRanges().forEach((Range t1) -> { + Line removedLine = projectionMap.remove(t1); + getChartChildren().removeAll(removedLine); }); }); - c.getAddedSubList().forEach((EventBundle t) -> { + c.getAddedSubList().forEach((DetailViewNode t) -> { - for (Range r : t.getRanges()) { + for (Range 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 impl clearGuideLine(); clearIntervalSelector(); - selectedBundles.clear(); + selectedNodes.clear(); projectionMap.clear(); controller.selectEventIDs(Collections.emptyList()); }); @@ -600,38 +588,38 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - private synchronized > double layoutNodes(final Collection nodes, final double minY, final double xOffset) { + private synchronized double layoutNodes(final Collection> 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 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 subNodes = n.getSubNodes(); + List> 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 spanWidths = new ArrayList<>(); - double x = getXAxis().getDisplayPosition(new DateTime(cn.getStartMillis()));; + double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));; double x2; - Iterator> ranges = cn.getStripe().getRanges().iterator(); + Iterator> ranges = stripeNode.getStripe().getRanges().iterator(); Range range = ranges.next(); do { x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); @@ -644,25 +632,31 @@ public final class EventDetailChart extends XYChart 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 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 impl return alternateLayout; } - private static class StartTimeComparator> implements Comparator { - - @Override - public int compare(T n1, T n2) { - return Long.compare(n1.getStartMillis(), n2.getStartMillis() - ); - } - } - private class DetailIntervalSelector extends IntervalSelector { public DetailIntervalSelector(double x, double height, Axis axis, TimeLineController controller) { @@ -797,12 +782,7 @@ public final class EventDetailChart extends XYChart 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); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 4e03ff342d..ac79c021a3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -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 { +public class EventStripeNode extends AbstractDetailViewNode { 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 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 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 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 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 getSubNodes() { - return subNodePane.getChildrenUnmodifiable().stream() - .map(EventStripeNode.class::cast) - .collect(Collectors.toList()); + Collection makeBundlesFromClusters(List 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 { - - @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 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> loggedTask = new LoggedTask>( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { - - @Override - protected List call() throws Exception { - //query for the sub-clusters - List 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 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(); - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java index 898917ae15..9971291863 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java @@ -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 p) -> new EventTreeCell()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java index e87b10cc17..cfd582433e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java @@ -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