From 750ef1335409e94125fd65192ccb6857494f2cad Mon Sep 17 00:00:00 2001 From: jmillman Date: Fri, 2 Oct 2015 17:11:15 -0400 Subject: [PATCH 01/13] subcluster expansion WIP --- .../timeline/datamodel/EventBundle.java | 13 +- .../timeline/datamodel/EventCluster.java | 37 +- .../timeline/datamodel/EventStripe.java | 59 ++-- .../ui/detailview/DetailViewPane.java | 20 +- .../ui/detailview/EventBundleNodeBase.java | 130 +++++++ .../ui/detailview/EventClusterNode.java | 296 ++++++++++++++++ .../ui/detailview/EventDetailChart.java | 282 +++++++++++---- .../ui/detailview/EventStripeNode.java | 320 +++--------------- .../tree/EventDescriptionTreeItem.java | 14 +- .../ui/detailview/tree/EventTypeTreeItem.java | 12 +- .../timeline/ui/detailview/tree/NavPanel.java | 24 +- .../ui/detailview/tree/NavTreeItem.java | 6 +- .../timeline/ui/detailview/tree/RootItem.java | 14 +- .../ui/detailview/tree/TreeComparator.java | 8 +- .../timeline/ui/filtering/FilterSetPanel.java | 2 +- 15 files changed, 797 insertions(+), 440 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java index 8714452725..070fa027dd 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java @@ -18,16 +18,16 @@ */ package org.sleuthkit.autopsy.timeline.datamodel; -import com.google.common.collect.Range; import java.util.Optional; import java.util.Set; +import java.util.SortedSet; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * */ -public interface EventBundle { +public interface EventBundle> { String getDescription(); @@ -35,7 +35,6 @@ public interface EventBundle { Set getEventIDs(); - Set getEventIDsWithHashHits(); Set getEventIDsWithTags(); @@ -46,11 +45,11 @@ public interface EventBundle { long getStartMillis(); - Iterable> getRanges(); + Optional getParentBundle(); - Optional getParentBundle(); - - default long getCount() { + default long getCount() { return getEventIDs().size(); } + + SortedSet getClusters(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index 57d5cec0db..a76bda8536 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -18,12 +18,14 @@ */ package org.sleuthkit.autopsy.timeline.datamodel; -import com.google.common.collect.Range; +import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Sets; import java.util.Collections; +import java.util.Comparator; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.SortedSet; import javax.annotation.concurrent.Immutable; import org.joda.time.Interval; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -36,7 +38,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; * designated 'zoom level', and be 'close together' in time. */ @Immutable -public class EventCluster implements EventBundle { +public class EventCluster implements EventBundle { /** * merge two event clusters into one new event cluster. @@ -62,7 +64,7 @@ public class EventCluster implements EventBundle { return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod); } - final private EventBundle parent; + final private EventStripe parent; /** * the smallest time interval containing all the clustered events @@ -101,7 +103,7 @@ public class EventCluster implements EventBundle { */ private final Set hashHits; - private EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLoD lod, EventBundle parent) { + private EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLoD lod, EventStripe parent) { this.span = spanningInterval; this.type = type; @@ -118,7 +120,7 @@ public class EventCluster implements EventBundle { } @Override - public Optional getParentBundle() { + public Optional getParentBundle() { return Optional.ofNullable(parent); } @@ -166,18 +168,13 @@ public class EventCluster implements EventBundle { return lod; } - Range getRange() { - if (getEndMillis() > getStartMillis()) { - return Range.closedOpen(getSpan().getStartMillis(), getSpan().getEndMillis()); - } else { - return Range.singleton(getStartMillis()); - } - } - - @Override - public Iterable> getRanges() { - return Collections.singletonList(getRange()); - } +// Range getRange() { +// if (getEndMillis() > getStartMillis()) { +// return Range.closedOpen(getSpan().getStartMillis(), getSpan().getEndMillis()); +// } else { +// return Range.singleton(getStartMillis()); +// } +// } /** * return a new EventCluster identical to this one, except with the given @@ -188,11 +185,15 @@ public class EventCluster implements EventBundle { * @return a new EventCluster identical to this one, except with the given * EventBundle as the parent. */ - public EventCluster withParent(EventBundle parent) { + public EventCluster withParent(EventStripe parent) { if (Objects.nonNull(this.parent)) { throw new IllegalStateException("Event Cluster already has a parent!"); } return new EventCluster(span, type, eventIDs, hashHits, tagged, description, lod, parent); } + @Override + public SortedSet< EventCluster> getClusters() { + return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(this).build(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index 8a465de1cd..86b822bc2c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -1,20 +1,31 @@ /* - * 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. + * Autopsy Forensic Browser + * + * Copyright 2015 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.sleuthkit.autopsy.timeline.datamodel; import com.google.common.base.Preconditions; -import com.google.common.collect.Range; -import com.google.common.collect.RangeMap; -import com.google.common.collect.RangeSet; -import com.google.common.collect.TreeRangeMap; -import com.google.common.collect.TreeRangeSet; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import javax.annotation.concurrent.Immutable; import org.python.google.common.base.Objects; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -25,7 +36,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; * description, and zoom levels. */ @Immutable -public final class EventStripe implements EventBundle { +public final class EventStripe implements EventBundle { public static EventStripe merge(EventStripe u, EventStripe v) { Preconditions.checkNotNull(u); @@ -37,10 +48,9 @@ public final class EventStripe implements EventBundle { return new EventStripe(u, v); } - private final EventBundle parent; + private final EventCluster parent; - private final RangeSet spans = TreeRangeSet.create(); - private final RangeMap spanMap = TreeRangeMap.create(); + private final SortedSet clusters = new TreeSet<>(Comparator.comparing(EventCluster::getStartMillis)); /** * the type of all the events @@ -73,23 +83,21 @@ public final class EventStripe implements EventBundle { */ private final Set hashHits = new HashSet<>(); - public EventStripe(EventCluster cluster) { - spans.add(cluster.getRange()); - spanMap.put(cluster.getRange(), cluster); + public EventStripe(EventCluster cluster, EventCluster parent) { + clusters.add(cluster); + type = cluster.getEventType(); description = cluster.getDescription(); lod = cluster.getDescriptionLoD(); eventIDs.addAll(cluster.getEventIDs()); tagged.addAll(cluster.getEventIDsWithTags()); hashHits.addAll(cluster.getEventIDsWithHashHits()); - parent = cluster.getParentBundle().orElse(null); + this.parent = parent; } private EventStripe(EventStripe u, EventStripe v) { - spans.addAll(u.spans); - spans.addAll(v.spans); - spanMap.putAll(u.spanMap); - spanMap.putAll(v.spanMap); + clusters.addAll(u.clusters); + clusters.addAll(v.clusters); type = u.getEventType(); description = u.getDescription(); lod = u.getDescriptionLoD(); @@ -103,7 +111,7 @@ public final class EventStripe implements EventBundle { } @Override - public Optional getParentBundle() { + public Optional getParentBundle() { return Optional.ofNullable(parent); } @@ -139,16 +147,15 @@ public final class EventStripe implements EventBundle { @Override public long getStartMillis() { - return spans.span().lowerEndpoint(); + return clusters.first().getStartMillis(); } @Override public long getEndMillis() { - return spans.span().upperEndpoint(); + return clusters.last().getEndMillis(); } - @Override - public Iterable> getRanges() { - return spans.asRanges(); + public SortedSet< EventCluster> getClusters() { + return Collections.unmodifiableSortedSet(clusters); } } 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 517cddde4e..b9ce5b8a7f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -89,11 +89,11 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; * EventTypeMap, and dataSets is all linked directly to the ClusterChart which * must only be manipulated on the JavaFx thread. */ -public class DetailViewPane extends AbstractVisualization { +public class DetailViewPane extends AbstractVisualization, EventDetailChart> { private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName()); - private MultipleSelectionModel> treeSelectionModel; + private MultipleSelectionModel>> treeSelectionModel; //these three could be injected from fxml but it was causing npe's private final DateAxis dateAxis = new DateAxis(); @@ -107,9 +107,9 @@ public class DetailViewPane extends AbstractVisualization highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - public ObservableList getEventBundles() { + public ObservableList> getEventBundles() { return chart.getEventBundles(); } @@ -136,7 +136,7 @@ public class DetailViewPane extends AbstractVisualization change) -> { + highlightedNodes.addListener((ListChangeListener.Change> change) -> { while (change.next()) { change.getAddedSubList().forEach(node -> { @@ -201,7 +201,7 @@ public class DetailViewPane extends AbstractVisualization { - for (EventStripeNode n : chart.getNodes((EventStripeNode t) -> + for (EventBundleNodeBase n : chart.getNodes((EventBundleNodeBase t) -> t.getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); } @@ -219,14 +219,14 @@ public class DetailViewPane extends AbstractVisualization> selectionModel) { + public void setSelectionModel(MultipleSelectionModel>> selectionModel) { this.treeSelectionModel = selectionModel; treeSelectionModel.getSelectedItems().addListener((Observable observable) -> { highlightedNodes.clear(); - for (TreeItem tn : treeSelectionModel.getSelectedItems()) { + for (TreeItem> tn : treeSelectionModel.getSelectedItems()) { - for (EventStripeNode n : chart.getNodes((EventStripeNode t) -> + for (EventBundleNodeBase n : chart.getNodes((EventBundleNodeBase t) -> t.getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); } @@ -351,7 +351,7 @@ public class DetailViewPane extends AbstractVisualization c1, Boolean selected) { c1.applySelectionEffect(selected); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java new file mode 100644 index 0000000000..b9ef1324a7 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -0,0 +1,130 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.Set; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Pane; +import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; +import static javafx.scene.layout.Region.USE_PREF_SIZE; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import org.controlsfx.control.action.ActionUtils; +import org.joda.time.DateTime; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventStripeNode.configureLoDButton; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; +import org.sleuthkit.datamodel.SleuthkitCase; + +/** + * + */ +public abstract class EventBundleNodeBase, ParentBundleType extends EventBundle, ParentNodeType extends EventBundleNodeBase> extends VBox { + + static final CornerRadii CORNER_RADII_3 = new CornerRadii(3); + static final CornerRadii CORNER_RADII_1 = new CornerRadii(1); + protected final EventDetailChart chart; + final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + protected final BundleType eventBundle; + + protected final ParentNodeType parentNode; + + final SleuthkitCase sleuthkitCase; + final FilteredEventsModel eventsModel; + + final Background highlightedBackground; + final Background defaultBackground; + final Color evtColor; +// final Button plusButton = ActionUtils.createButton(new EventStripeNode.ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE); +// final Button minusButton = ActionUtils.createButton(new EventStripeNode.CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE); + final Button hideButton; + final List subNodes = new ArrayList<>(); + final Pane subNodePane = new Pane(); + + public EventBundleNodeBase(EventDetailChart chart, BundleType eventBundle, ParentNodeType parentNode) { + this.eventBundle = eventBundle; + this.parentNode = parentNode; + this.chart = chart; + + this.descLOD.set(eventBundle.getDescriptionLoD()); + sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); + eventsModel = chart.getController().getEventsModel(); + evtColor = getEventType().getColor(); + defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY)); + highlightedBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY)); + + setBackground(defaultBackground); + EventDetailChart.HideDescriptionAction hideClusterAction = chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD()); + hideButton = ActionUtils.createButton(hideClusterAction, ActionUtils.ActionTextBehavior.HIDE); + configureLoDButton(hideButton); +// configureLoDButton(plusButton); +// configureLoDButton(minusButton); + + setAlignment(Pos.TOP_LEFT); + setMinHeight(24); + setPrefHeight(USE_COMPUTED_SIZE); + setMaxHeight(USE_PREF_SIZE); + setMaxWidth(USE_PREF_SIZE); + setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()); + } + + final DescriptionLoD getDescriptionLoD() { + return descLOD.get(); + } + + final public BundleType getEventBundle() { + return eventBundle; + } + + final double getLayoutXCompensation() { + return (parentNode != null ? parentNode.getLayoutXCompensation() : 0) + + getBoundsInParent().getMinX(); + } + + final EventType getEventType() { + return getEventBundle().getEventType(); + } + + final String getDescription() { + return getEventBundle().getDescription(); + } + + final long getStartMillis() { + return getEventBundle().getStartMillis(); + } + + final Set getEventsIDs() { + return getEventBundle().getEventIDs(); + } + + abstract void installTooltip(); + + abstract void applySelectionEffect(boolean selected); + + /** + * apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ + abstract void applyHighlightEffect(boolean applied); + + @SuppressWarnings("unchecked") + public List getSubNodes() { + return subNodes; + } + + abstract void setDescriptionVisibility(DescriptionVisibility get); +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java new file mode 100644 index 0000000000..7d6c805633 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -0,0 +1,296 @@ +/* + * 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.Collections; +import java.util.List; +import static java.util.Objects.nonNull; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javafx.beans.binding.Bindings; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.SeparatorMenuItem; +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 org.controlsfx.control.action.Action; +import org.controlsfx.control.action.ActionUtils; +import org.joda.time.Interval; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; +import org.sleuthkit.autopsy.timeline.filters.RootFilter; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.CORNER_RADII_3; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; + +/** + * + */ +public class EventClusterNode extends EventBundleNodeBase { + + private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); + private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1); + private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); + + public EventClusterNode(EventDetailChart chart, EventCluster eventCluster, EventStripeNode parentNode) { + super(chart, eventCluster, parentNode); + + + setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII_3, Insets.EMPTY))); + setBorder(clusterBorder); + + setCursor(Cursor.HAND); + setOnMouseClicked(new MouseClickHandler()); + + //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); + }); + + } + + @Override + final void installTooltip() { + + } + + @Override + void applySelectionEffect(boolean selected) { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + void applyHighlightEffect(boolean applied) { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + void setSpanWidths(List spanWidths) { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + void setDescriptionWidth(double max) { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + void setDescriptionVisibility(DescriptionVisibility get) { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + /** + * event handler used for mouse events on {@link EventStripeNode}s + */ + private class MouseClickHandler implements EventHandler { + + private ContextMenu contextMenu; + + @Override + public void handle(MouseEvent t) { + + if (t.getButton() == MouseButton.PRIMARY) { + t.consume(); + if (t.isShiftDown()) { + if (chart.selectedNodes.contains(EventClusterNode.this) == false) { + chart.selectedNodes.add(EventClusterNode.this); + } + } else if (t.isShortcutDown()) { + chart.selectedNodes.removeAll(EventClusterNode.this); + } else if (t.getClickCount() > 1) { + final DescriptionLoD next = descLOD.get().moreDetailed(); + if (next != null) { + loadSubBundles(DescriptionLoD.RelativeDetail.MORE); + + } + } else { + chart.selectedNodes.setAll(EventClusterNode.this); + } + t.consume(); + } else if (t.getButton() == MouseButton.SECONDARY) { + ContextMenu chartContextMenu = chart.getChartContextMenu(t); + if (contextMenu == null) { + contextMenu = new ContextMenu(); + contextMenu.setAutoHide(true); + + contextMenu.getItems().add(ActionUtils.createMenuItem(new ExpandClusterAction())); + contextMenu.getItems().add(ActionUtils.createMenuItem(new CollapseClusterAction())); + + contextMenu.getItems().add(new SeparatorMenuItem()); + contextMenu.getItems().addAll(chartContextMenu.getItems()); + } + contextMenu.show(EventClusterNode.this, t.getScreenX(), t.getScreenY()); + t.consume(); + } + } + } + + /** + * loads sub-bundles at the given Description LOD, continues + * + * @param requestedDescrLoD + * @param expand + */ + @NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters") + private synchronized void loadSubBundles(DescriptionLoD.RelativeDetail relativeDetail) { + chart.getEventBundles().removeIf(bundle -> + getSubNodes().stream().anyMatch(subNode -> + bundle.equals(subNode.getEventStripe())) + ); + subNodePane.getChildren().clear(); + if (descLOD.get().withRelativeDetail(relativeDetail) == getEventBundle().getDescriptionLoD()) { + descLOD.set(getEventBundle().getDescriptionLoD()); +// clustersHBox.setVisible(true); + chart.setRequiresLayout(true); + chart.requestChartLayout(); + } else { +// clustersHBox.setVisible(false); + + // make new ZoomParams to query with + final RootFilter subClusterFilter = null;//getSubClusterFilter(); + /* + * We need to extend end time because for the query by one second, + * because it is treated as an open interval but we want to include + * events at exactly the time of the last event in this cluster + */ + final Interval subClusterSpan = new Interval(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000); + final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); + final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); + + Task> loggedTask = new Task>() { + + private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); + + { + updateTitle(Bundle.EventStripeNode_loggedTask_name()); + } + + @Override + protected Collection call() throws Exception { + Collection bundles; + DescriptionLoD next = loadedDescriptionLoD; + do { + loadedDescriptionLoD = next; + if (loadedDescriptionLoD == getEventBundle().getDescriptionLoD()) { + return Collections.emptySet(); + } + bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream() + // .map(cluster -> cluster.withParent(getEventBundle())) + .collect(Collectors.toMap((eventCluster) -> eventCluster.getDescription(), //key + (eventCluster) -> new EventStripe(eventCluster, getEventCluster()), //value + EventStripe::merge) //merge method + ).values(); + next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); + } while (bundles.size() == 1 && nonNull(next)); + + // return list of AbstractEventStripeNodes representing sub-bundles + return bundles; + + } + + @Override + protected void succeeded() { + chart.setCursor(Cursor.WAIT); + try { + Collection bundles = get(); + + if (bundles.isEmpty()) { +// clustersHBox.setVisible(true); + } else { +// clustersHBox.setVisible(false); + chart.getEventBundles().addAll(bundles); + subNodePane.getChildren().setAll(bundles.stream() + .map(EventClusterNode.this::getSubNodeForBundle) + .collect(Collectors.toSet())); + } + descLOD.set(loadedDescriptionLoD); + //assign subNodes and request chart layout + + chart.setRequiresLayout(true); + chart.requestChartLayout(); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); + } + chart.setCursor(null); + } + }; + +//start task + chart.getController().monitorTask(loggedTask); + } + } + + EventStripeNode getSubNodeForBundle(EventStripe stripe) { + return new EventStripeNode(chart, stripe, this); + } + + private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N + private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N + + private class ExpandClusterAction extends Action { + + @NbBundle.Messages("ExpandClusterAction.text=Expand") + ExpandClusterAction() { + super(Bundle.ExpandClusterAction_text()); + + setGraphic(new ImageView(PLUS)); + setEventHandler((ActionEvent t) -> { + final DescriptionLoD next = descLOD.get().moreDetailed(); + if (next != null) { + loadSubBundles(DescriptionLoD.RelativeDetail.MORE); + + } + }); + disabledProperty().bind(descLOD.isEqualTo(DescriptionLoD.FULL)); + } + } + + private class CollapseClusterAction extends Action { + + @NbBundle.Messages("CollapseClusterAction.text=Collapse") + CollapseClusterAction() { + super(Bundle.CollapseClusterAction_text()); + + setGraphic(new ImageView(MINUS)); + setEventHandler((ActionEvent t) -> { + final DescriptionLoD previous = descLOD.get().lessDetailed(); + if (previous != null) { + loadSubBundles(DescriptionLoD.RelativeDetail.LESS); + } + }); + disabledProperty().bind(Bindings.createBooleanBinding(() -> nonNull(getEventCluster()) && descLOD.get() == getEventCluster().getDescriptionLoD(), descLOD)); + } + } + + EventCluster getEventCluster() { + return getEventBundle(); + } +} 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 da07263e9f..e87ecc2a59 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import com.google.common.collect.Range; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -145,16 +144,16 @@ public final class EventDetailChart extends XYChart impl @GuardedBy(value = "this") private boolean requiresLayout = true; - final ObservableList selectedNodes; + final ObservableList> selectedNodes; /** * the group that all event nodes are added to. This facilitates scrolling * by allowing a single translation of this group. */ private final Group nodeGroup = new Group(); - private final ObservableList bundles = FXCollections.observableArrayList(); + private final ObservableList> bundles = FXCollections.observableArrayList(); private final Map, EventStripe> stripeDescMap = new HashMap<>(); private final Map stripeNodeMap = new HashMap<>(); - private final Map, Line> projectionMap = new HashMap<>(); + private final Map projectionMap = new HashMap<>(); /** * list of series of data added to this chart @@ -202,7 +201,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); @@ -255,24 +254,24 @@ public final class EventDetailChart extends XYChart impl this.selectedNodes = selectedNodes; this.selectedNodes.addListener(( - ListChangeListener.Change c) -> { - while (c.next()) { - c.getRemoved().forEach((EventStripeNode t) -> { - t.getEventStripe().getRanges().forEach((Range t1) -> { + ListChangeListener.Change> change) -> { + while (change.next()) { + change.getRemoved().forEach((EventBundleNodeBase removedNode) -> { + removedNode.getEventBundle().getClusters().forEach(cluster -> { - Line removedLine = projectionMap.remove(t1); + Line removedLine = projectionMap.remove(cluster); getChartChildren().removeAll(removedLine); }); }); - c.getAddedSubList().forEach((EventStripeNode t) -> { + change.getAddedSubList().forEach((EventBundleNodeBase addedNode) -> { - for (Range range : t.getEventStripe().getRanges()) { + for (EventCluster range : addedNode.getEventBundle().getClusters()) { - 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 line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, + dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET ); - line.setStroke(t.getEventType().getColor().deriveColor(0, 1, 1, .5)); + line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5)); line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); line.setStrokeLineCap(StrokeLineCap.ROUND); projectionMap.put(range, line); @@ -289,7 +288,7 @@ public final class EventDetailChart extends XYChart impl requestChartLayout(); } - ObservableList getEventBundles() { + ObservableList> getEventBundles() { return bundles; } @@ -400,7 +399,7 @@ public final class EventDetailChart extends XYChart impl final EventCluster eventCluster = data.getYValue(); bundles.add(eventCluster); EventStripe eventStripe = stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()), - new EventStripe(eventCluster), + new EventStripe(eventCluster, null), (EventStripe u, EventStripe v) -> { EventStripeNode remove = stripeNodeMap.remove(u); nodeGroup.getChildren().remove(remove); @@ -482,7 +481,7 @@ public final class EventDetailChart extends XYChart impl .filter(AbstractFilter::isActive) .anyMatch(filter -> filter.getDescription().equals(node.getDescription())))); - layoutNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, 0); + layoutStripeNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, 0); minY = maxY.get(); } } else { @@ -490,7 +489,7 @@ public final class EventDetailChart extends XYChart impl .collect(Collectors.partitioningBy(node -> getController().getQuickHideFilters().stream() .filter(AbstractFilter::isActive) .anyMatch(filter -> filter.getDescription().equals(node.getDescription())))); - layoutNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), 0, 0); + layoutStripeNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), 0, 0); } setCursor(null); requiresLayout = false; @@ -508,7 +507,7 @@ public final class EventDetailChart extends XYChart impl * * @return the double */ - private double layoutNodesHelper(List hiddenNodes, List shownNodes, double minY, final double xOffset) { + private double layoutStripeNodesHelper(List hiddenNodes, List shownNodes, double minY, final double xOffset) { hiddenNodes.forEach((EventStripeNode t) -> { // children.remove(t); @@ -525,7 +524,37 @@ public final class EventDetailChart extends XYChart impl }); shownNodes.sort(Comparator.comparing(EventStripeNode::getStartMillis)); - return layoutNodes(shownNodes, minY, xOffset); + return layoutStripeNodes(shownNodes, minY, xOffset); + } + + /** + * + * @param hiddenNodes the value of hiddenNodes + * @param shownNodes the value of shownNodes + * @param minY the value of minY + * @param children the value of children + * @param xOffset the value of xOffset + * + * @return the double + */ + private double layoutClusterNodesHelper(List hiddenNodes, List shownNodes, double minY, final double xOffset) { + + hiddenNodes.forEach((EventClusterNode t) -> { +// children.remove(t); + t.setVisible(false); + t.setManaged(false); + }); + + shownNodes.forEach((EventClusterNode t) -> { +// if (false == children.contains(t)) { +// children.add(t); +// } + t.setVisible(true); + t.setManaged(true); + }); + + shownNodes.sort(Comparator.comparing(EventClusterNode::getStartMillis)); + return layoutClusterNodes(shownNodes, minY, xOffset); } @Override @@ -549,24 +578,35 @@ public final class EventDetailChart extends XYChart impl ReadOnlyDoubleProperty maxVScrollProperty() { return maxY.getReadOnlyProperty(); } + Function>> clusterFlattener = + new Function>>() { + @Override + public Stream> apply(EventClusterNode node) { + return Stream.concat( + Stream.of(node), + node.getSubNodes().stream().flatMap(stripeFlattener::apply)); + } + }; - Iterable getNodes(Predicate p) { - Function> flattener = - new Function>() { - @Override - public Stream apply(EventStripeNode node) { - return Stream.concat( - Stream.of(node), - node.getSubNodes().stream().flatMap(this::apply)); - } - }; + Function>> stripeFlattener = + new Function>>() { + + @Override + public Stream> apply(EventStripeNode node) { + return Stream.concat( + Stream.of(node), + node.getSubNodes().stream().flatMap(clusterFlattener::apply)); + } + }; + + Iterable> getNodes(Predicate> p) { return stripeNodeMap.values().stream() - .flatMap(flattener) + .flatMap(stripeFlattener) .filter(p).collect(Collectors.toList()); } - Iterable getAllNodes() { + Iterable> getAllNodes() { return getNodes(x -> true); } @@ -587,12 +627,11 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - private synchronized double layoutNodes(final Collection< EventStripeNode> 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 + private synchronized double layoutStripeNodes(final Collection< EventStripeNode> nodes, final double minY, final double xOffset) { + //hash map from y value to right most occupied x value. This tells you for a given 'pixel 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 each node size it and position it in first available slot for (EventStripeNode stripeNode : nodes) { stripeNode.setDescriptionVisibility(descrVisibility.get()); @@ -601,37 +640,26 @@ public final class EventDetailChart extends XYChart impl //position of start and end according to range of axis double startX = rawDisplayPosition - xOffset; double layoutNodesResultHeight = 0; - double span = 0; - List subNodes = stripeNode.getSubNodes(); - if (subNodes.isEmpty() == false) { - Map> hiddenPartition = subNodes.stream() - .collect(Collectors.partitioningBy(testNode -> getController().getQuickHideFilters().stream() - .filter(AbstractFilter::isActive) - .anyMatch(filter -> filter.getDescription().equals(testNode.getDescription())))); - - layoutNodesResultHeight = layoutNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, rawDisplayPosition); - } - List spanWidths = new ArrayList<>(); double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));; double x2; - Iterator> ranges = stripeNode.getEventStripe().getRanges().iterator(); - Range range = ranges.next(); + Iterator ranges = stripeNode.getEventStripe().getClusters().iterator(); + EventCluster cluster = ranges.next(); do { - x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); + x2 = getXAxis().getDisplayPosition(new DateTime(cluster.getEndMillis())); double clusterSpan = x2 - x; span += clusterSpan; spanWidths.add(clusterSpan); if (ranges.hasNext()) { - range = ranges.next(); - x = getXAxis().getDisplayPosition(new DateTime(range.lowerEndpoint())); + cluster = ranges.next(); + x = getXAxis().getDisplayPosition(new DateTime(cluster.getStartMillis())); double gapSpan = x - x2; span += gapSpan; spanWidths.add(gapSpan); if (ranges.hasNext() == false) { - x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); + x2 = getXAxis().getDisplayPosition(new DateTime(cluster.getEndMillis())); clusterSpan = x2 - x; span += clusterSpan; spanWidths.add(clusterSpan); @@ -647,6 +675,15 @@ public final class EventDetailChart extends XYChart impl } else { //else set it unbounded stripeNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); } + List subNodes = stripeNode.getSubNodes(); + if (subNodes.isEmpty() == false) { + Map> hiddenPartition = subNodes.stream() + .collect(Collectors.partitioningBy(testNode -> getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(testNode.getDescription())))); + + layoutNodesResultHeight = layoutClusterNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, rawDisplayPosition); + } stripeNode.autosize(); //compute size of tlNode based on constraints and event data @@ -654,10 +691,10 @@ public final class EventDetailChart extends XYChart impl double xRight = startX + stripeNode.getWidth(); //get the height of the node - final double h = layoutNodesResultHeight == 0 ? stripeNode.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; + final double h = layoutNodesResultHeight == 0 ? stripeNode.getHeight() : layoutNodesResultHeight ; + //initial test position double yPos = minY; - double yPos2 = yPos + h; if (oneEventPerRow.get()) { @@ -665,8 +702,7 @@ public final class EventDetailChart extends XYChart impl yPos = (localMax + 2); yPos2 = yPos + h; - } else {//else - + } else { boolean overlapping = true; while (overlapping) { //loop through y values looking for available slot. @@ -702,13 +738,135 @@ public final class EventDetailChart extends XYChart impl return localMax - minY; } + /** + * layout the nodes in the given list, starting form the given minimum y + * coordinate. + * + * @param nodes + * @param minY + */ + private synchronized double layoutClusterNodes(final Collection< EventClusterNode> 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 (EventClusterNode clusterNode : nodes) { + + clusterNode.setDescriptionVisibility(descrVisibility.get()); + double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(clusterNode.getStartMillis())); + + //position of start and end according to range of axis + double startX = rawDisplayPosition - xOffset; + double layoutNodesResultHeight = 0; + + double span = 0; + + List spanWidths = new ArrayList<>(); + double x = getXAxis().getDisplayPosition(new DateTime(clusterNode.getStartMillis()));; + double x2; + Iterator ranges = clusterNode.getEventCluster().getClusters().iterator(); + EventCluster range = ranges.next(); + do { + x2 = getXAxis().getDisplayPosition(new DateTime(range.getEndMillis())); + double clusterSpan = x2 - x; + span += clusterSpan; + spanWidths.add(clusterSpan); + if (ranges.hasNext()) { + range = ranges.next(); + x = getXAxis().getDisplayPosition(new DateTime(range.getStartMillis())); + double gapSpan = x - x2; + span += gapSpan; + spanWidths.add(gapSpan); + if (ranges.hasNext() == false) { + x2 = getXAxis().getDisplayPosition(new DateTime(range.getEndMillis())); + clusterSpan = x2 - x; + span += clusterSpan; + spanWidths.add(clusterSpan); + } + } + + } while (ranges.hasNext()); + + clusterNode.setSpanWidths(spanWidths); + + if (truncateAll.get()) { //if truncate option is selected limit width of description label + clusterNode.setDescriptionWidth(Math.max(span, truncateWidth.get())); + } else { //else set it unbounded + clusterNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); + } + + List subNodes = clusterNode.getSubNodes(); + if (subNodes.isEmpty() == false) { + Map> hiddenPartition = subNodes.stream() + .collect(Collectors.partitioningBy(testNode -> getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(testNode.getDescription())))); + + layoutNodesResultHeight = layoutStripeNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, rawDisplayPosition); + } + + clusterNode.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 + clusterNode.getWidth(); + + //get the height of the node + final double h = layoutNodesResultHeight == 0 ? clusterNode.getHeight() : layoutNodesResultHeight ; + //initial test position + double yPos = minY; + + double yPos2 = yPos + h; + + if (oneEventPerRow.get()) { + // if onePerRow, just put it at end + yPos = (localMax + 2); + yPos2 = yPos + h; + + } else {//else + + boolean overlapping = true; + while (overlapping) { + //loop through y values looking for available slot. + + overlapping = false; + //check each pixel from top to bottom. + for (double y = yPos2; y >= yPos; y--) { + final Double maxX = maxXatY.get((int) y); + if (maxX != null && maxX >= startX - 4) { + //if that pixel is already used + //jump top to this y value and repeat until free slot is found. + overlapping = true; + yPos = y + 4; + yPos2 = yPos + h; + break; + } + } + } + //mark used y values + for (double y = yPos; y <= yPos2; y++) { + maxXatY.put((int) y, xRight); + } + } + localMax = Math.max(yPos2, localMax); + + Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), + new KeyValue(clusterNode.layoutXProperty(), startX), + new KeyValue(clusterNode.layoutYProperty(), yPos))); + + tm.play(); + } + maxY.set(Math.max(maxY.get(), localMax)); + return localMax - minY; + } + private void layoutProjectionMap() { - for (final Map.Entry, Line> entry : projectionMap.entrySet()) { - final Range range = entry.getKey(); + for (final Map.Entry entry : projectionMap.entrySet()) { + final EventCluster cluster = entry.getKey(); final Line line = entry.getValue(); - line.setStartX(getParentXForEpochMillis(range.lowerEndpoint())); - line.setEndX(getParentXForEpochMillis(range.upperEndpoint())); + line.setStartX(getParentXForEpochMillis(cluster.getStartMillis())); + line.setEndX(getParentXForEpochMillis(cluster.getEndMillis())); line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 666b0e01c8..3cd77b26e9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -19,26 +19,18 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import com.google.common.collect.Range; -import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import static java.util.Objects.nonNull; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.stream.Collectors; -import javafx.beans.binding.Bindings; -import javafx.beans.property.SimpleObjectProperty; import javafx.concurrent.Task; -import javafx.event.ActionEvent; 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.ContextMenu; @@ -51,56 +43,37 @@ 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.Region; 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.controlsfx.control.action.Action; -import org.controlsfx.control.action.ActionUtils; -import org.joda.time.DateTime; -import org.joda.time.Interval; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; -import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; -import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; -import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; /** * Node used in {@link EventDetailChart} to represent an EventStripe. */ -final public class EventStripeNode extends StackPane { +final public class EventStripeNode extends EventBundleNodeBase { private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName()); private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N - private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N - private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N + private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N - private static final CornerRadii CORNER_RADII_3 = new CornerRadii(3); - private static final CornerRadii CORNER_RADII_1 = new CornerRadii(1); - private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1); private final static Map dropShadowMap = new ConcurrentHashMap<>(); private static final Border SELECTION_BORDER = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2))); @@ -116,7 +89,6 @@ final public class EventStripeNode extends StackPane { b.setManaged(show); } - private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); private DescriptionVisibility descrVis; private Tooltip tooltip; @@ -127,46 +99,19 @@ final public class EventStripeNode extends StackPane { * //TODO: move more of the control of subnodes/events here and out of * EventDetail Chart */ - private final Pane subNodePane = new Pane(); - private final HBox clustersHBox = new HBox(); +// private final HBox clustersHBox = new HBox(); private final ImageView eventTypeImageView = new ImageView(); private final Label descrLabel = new Label("", eventTypeImageView); private final Label countLabel = new Label(); - private final Button plusButton = ActionUtils.createButton(new ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE); - private final Button minusButton = ActionUtils.createButton(new CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE); + private final ImageView hashIV = new ImageView(HASH_PIN); private final ImageView tagIV = new ImageView(TAG); - private final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV, minusButton, plusButton); + private final HBox infoHBox; - private final Background highlightedBackground; - private final Background defaultBackground; - private final EventDetailChart chart; - private final SleuthkitCase sleuthkitCase; - private final EventStripe eventStripe; - private final EventStripeNode parentNode; - private final FilteredEventsModel eventsModel; - private final Button hideButton; + public EventStripeNode(EventDetailChart chart, EventStripe eventStripe, EventClusterNode parentNode) { + super(chart, eventStripe, parentNode); - public EventStripeNode(EventDetailChart chart, EventStripe eventStripe, EventStripeNode parentEventNode) { - this.eventStripe = eventStripe; - this.parentNode = parentEventNode; - this.chart = chart; - descLOD.set(eventStripe.getDescriptionLoD()); - sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); - eventsModel = chart.getController().getEventsModel(); - final Color evtColor = getEventType().getColor(); - defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY)); - highlightedBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY)); - - setBackground(defaultBackground); - - setAlignment(Pos.TOP_LEFT); - setMinHeight(24); - setPrefHeight(USE_COMPUTED_SIZE); - setMaxHeight(USE_PREF_SIZE); - setMaxWidth(USE_PREF_SIZE); - minWidthProperty().bind(clustersHBox.widthProperty()); - setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventStripe.getStartMillis())) - getLayoutXCompensation()); + minWidthProperty().bind(subNodePane.widthProperty()); if (eventStripe.getEventIDsWithHashHits().isEmpty()) { show(hashIV, false); @@ -175,18 +120,16 @@ final public class EventStripeNode extends StackPane { show(tagIV, false); } - EventDetailChart.HideDescriptionAction hideClusterAction = chart.new HideDescriptionAction(getDescription(), eventStripe.getDescriptionLoD()); - hideButton = ActionUtils.createButton(hideClusterAction, ActionUtils.ActionTextBehavior.HIDE); - configureLoDButton(hideButton); - configureLoDButton(plusButton); - configureLoDButton(minusButton); + infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV/* + * , minusButton, plusButton + */); //initialize info hbox infoHBox.getChildren().add(4, hideButton); - infoHBox.setMinWidth(USE_PREF_SIZE); infoHBox.setPadding(new Insets(2, 5, 2, 5)); infoHBox.setAlignment(Pos.CENTER_LEFT); + infoHBox.setPickOnBounds(false); //setup description label eventTypeImageView.setImage(getEventType().getFXImage()); @@ -200,23 +143,19 @@ final public class EventStripeNode extends StackPane { subNodePane.setMinWidth(USE_PREF_SIZE); subNodePane.setMaxHeight(USE_PREF_SIZE); subNodePane.setMaxWidth(USE_PREF_SIZE); - subNodePane.setPickOnBounds(false); +// subNodePane.setPickOnBounds(false); - Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); - for (Range range : eventStripe.getRanges()) { - Region clusterRegion = new Region(); - clusterRegion.setBorder(clusterBorder); - clusterRegion.setBackground(highlightedBackground); - clustersHBox.getChildren().addAll(clusterRegion, new Region()); + for (EventCluster cluster : eventStripe.getClusters()) { + EventClusterNode clusterNode = new EventClusterNode(chart, cluster, this); + subNodes.add(clusterNode); + subNodePane.getChildren().addAll(clusterNode, new Region()); } - clustersHBox.getChildren().remove(clustersHBox.getChildren().size() - 1); - clustersHBox.setMaxWidth(USE_PREF_SIZE); + subNodePane.getChildren().remove(subNodePane.getChildren().size() - 1); +// clustersHBox.setMaxWidth(USE_PREF_SIZE); - final VBox internalVBox = new VBox(infoHBox, subNodePane); - internalVBox.setAlignment(Pos.CENTER_LEFT); - getChildren().addAll(clustersHBox, internalVBox); + getChildren().addAll( infoHBox,subNodePane); - setCursor(Cursor.HAND); +// setCursor(Cursor.HAND); setOnMouseClicked(new MouseClickHandler()); //set up mouse hover effect and tooltip @@ -233,20 +172,21 @@ final public class EventStripeNode extends StackPane { setOnMouseExited((MouseEvent e) -> { showDescriptionLoDControls(false); }); + } void showDescriptionLoDControls(final boolean showControls) { DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), eventType -> new DropShadow(10, eventType.getColor())); - clustersHBox.setEffect(showControls ? dropShadow : null); - show(minusButton, showControls); - show(plusButton, showControls); + subNodePane.setEffect(showControls ? dropShadow : null); +// show(minusButton, showControls); +// show(plusButton, showControls); show(hideButton, showControls); } public void setSpanWidths(List spanWidths) { for (int i = 0; i < spanWidths.size(); i++) { - Region spanRegion = (Region) clustersHBox.getChildren().get(i); + Region spanRegion = (Region) subNodePane.getChildren().get(i); Double w = spanWidths.get(i); spanRegion.setPrefWidth(w); @@ -256,24 +196,23 @@ final public class EventStripeNode extends StackPane { } public EventStripe getEventStripe() { - return eventStripe; - } - - Collection makeBundlesFromClusters(List eventClusters) { - return eventClusters.stream().collect( - Collectors.toMap( - EventCluster::getDescription, //key - EventStripe::new, //value - EventStripe::merge)//merge method - ).values(); + return getEventBundle(); } +// Collection makeBundlesFromClusters(List eventClusters) { +// return eventClusters.stream().collect( +// Collectors.toMap((eventCluster) -> eventCluster.getDescription(), //key +// (eventCluster) -> new EventStripe(eventCluster,this), //value +// EventStripe::merge)//merge method +// ).values(); +// } @NbBundle.Messages({"# {0} - counts", "# {1} - event type", "# {2} - description", "# {3} - start date/time", "# {4} - end date/time", "EventStripeNode.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}"}) + @Override synchronized void installTooltip() { if (tooltip == null) { final Task tooltTipTask = new Task() { @@ -281,10 +220,10 @@ final public class EventStripeNode extends StackPane { @Override protected String call() throws Exception { HashMap hashSetCounts = new HashMap<>(); - if (!eventStripe.getEventIDsWithHashHits().isEmpty()) { + if (!getEventStripe().getEventIDsWithHashHits().isEmpty()) { hashSetCounts = new HashMap<>(); try { - for (TimeLineEvent tle : eventsModel.getEventsById(eventStripe.getEventIDsWithHashHits())) { + for (TimeLineEvent tle : eventsModel.getEventsById(getEventStripe().getEventIDsWithHashHits())) { Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); for (String hashSetName : hashSetNames) { hashSetCounts.merge(hashSetName, 1L, Long::sum); @@ -331,29 +270,6 @@ final public class EventStripeNode extends StackPane { } } - EventStripeNode getNodeForBundle(EventStripe cluster) { - return new EventStripeNode(chart, cluster, this); - } - - EventType getEventType() { - return eventStripe.getEventType(); - } - - String getDescription() { - return eventStripe.getDescription(); - } - - long getStartMillis() { - return eventStripe.getStartMillis(); - } - - @SuppressWarnings("unchecked") - public List getSubNodes() { - return subNodePane.getChildrenUnmodifiable().stream() - .map(t -> (EventStripeNode) t) - .collect(Collectors.toList()); - } - /** * make a new filter intersecting the global filter with description and * type filters to restrict sub-clusters @@ -362,7 +278,7 @@ final public class EventStripeNode extends StackPane { RootFilter getSubClusterFilter() { RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf(); subClusterFilter.getSubFilters().addAll( - new DescriptionFilter(eventStripe.getDescriptionLoD(), eventStripe.getDescription(), DescriptionFilter.FilterMode.INCLUDE), + new DescriptionFilter(getEventStripe().getDescriptionLoD(), getEventStripe().getDescription(), DescriptionFilter.FilterMode.INCLUDE), new TypeFilter(getEventType())); return subClusterFilter; } @@ -379,6 +295,7 @@ final public class EventStripeNode extends StackPane { * * @param applied true to apply the selection 'effect', false to remove it */ + @Override public void applySelectionEffect(boolean applied) { setBorder(applied ? SELECTION_BORDER : null); } @@ -388,6 +305,7 @@ final public class EventStripeNode extends StackPane { * * @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 @@ -398,114 +316,10 @@ final public class EventStripeNode extends StackPane { } } - private DescriptionLoD getDescriptionLoD() { - return descLOD.get(); - } - - /** - * loads sub-bundles at the given Description LOD, continues - * - * @param requestedDescrLoD - * @param expand - */ - @NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters") - private synchronized void loadSubBundles(DescriptionLoD.RelativeDetail relativeDetail) { - chart.getEventBundles().removeIf(bundle -> - getSubNodes().stream().anyMatch(subNode -> - bundle.equals(subNode.getEventStripe())) - ); - subNodePane.getChildren().clear(); - if (descLOD.get().withRelativeDetail(relativeDetail) == eventStripe.getDescriptionLoD()) { - descLOD.set(eventStripe.getDescriptionLoD()); - clustersHBox.setVisible(true); - chart.setRequiresLayout(true); - chart.requestChartLayout(); - } else { - clustersHBox.setVisible(false); - - // make new ZoomParams to query with - final RootFilter subClusterFilter = getSubClusterFilter(); - /* - * We need to extend end time because for the query by one second, - * because it is treated as an open interval but we want to include - * events at exactly the time of the last event in this cluster - */ - final Interval subClusterSpan = new Interval(eventStripe.getStartMillis(), eventStripe.getEndMillis() + 1000); - final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); - final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); - - Task> loggedTask = new Task>() { - - private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); - - { - updateTitle(Bundle.EventStripeNode_loggedTask_name()); - } - - @Override - protected Collection call() throws Exception { - Collection bundles; - DescriptionLoD next = loadedDescriptionLoD; - do { - loadedDescriptionLoD = next; - if (loadedDescriptionLoD == eventStripe.getDescriptionLoD()) { - return Collections.emptySet(); - } - bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream() - .map(cluster -> cluster.withParent(getEventStripe())) - .collect(Collectors.toMap( - EventCluster::getDescription, //key - EventStripe::new, //value - EventStripe::merge) //merge method - ).values(); - next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); - } while (bundles.size() == 1 && nonNull(next)); - - // return list of AbstractEventStripeNodes representing sub-bundles - return bundles; - - } - - @Override - protected void succeeded() { - chart.setCursor(Cursor.WAIT); - try { - Collection bundles = get(); - - if (bundles.isEmpty()) { - clustersHBox.setVisible(true); - } else { - clustersHBox.setVisible(false); - chart.getEventBundles().addAll(bundles); - subNodePane.getChildren().setAll(bundles.stream() - .map(EventStripeNode.this::getNodeForBundle) - .collect(Collectors.toSet())); - } - descLOD.set(loadedDescriptionLoD); - //assign subNodes and request chart layout - - chart.setRequiresLayout(true); - chart.requestChartLayout(); - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); - } - chart.setCursor(null); - } - }; - -//start task - chart.getController().monitorTask(loggedTask); - } - } - - private double getLayoutXCompensation() { - return (parentNode != null ? parentNode.getLayoutXCompensation() : 0) - + getBoundsInParent().getMinX(); - } - + @Override public void setDescriptionVisibility(DescriptionVisibility descrVis) { this.descrVis = descrVis; - final int size = eventStripe.getEventIDs().size(); + final int size = getEventStripe().getEventIDs().size(); switch (this.descrVis) { case HIDDEN: @@ -518,7 +332,7 @@ final public class EventStripeNode extends StackPane { break; default: case SHOWN: - String description = eventStripe.getDescription(); + String description = getEventStripe().getDescription(); description = parentNode != null ? " ..." + StringUtils.substringAfter(description, parentNode.getDescription()) : description; @@ -528,10 +342,6 @@ final public class EventStripeNode extends StackPane { } } - Set getEventsIDs() { - return eventStripe.getEventIDs(); - } - /** * event handler used for mouse events on {@link EventStripeNode}s */ @@ -550,12 +360,6 @@ final public class EventStripeNode extends StackPane { } } else if (t.isShortcutDown()) { chart.selectedNodes.removeAll(EventStripeNode.this); - } else if (t.getClickCount() > 1) { - final DescriptionLoD next = descLOD.get().moreDetailed(); - if (next != null) { - loadSubBundles(DescriptionLoD.RelativeDetail.MORE); - - } } else { chart.selectedNodes.setAll(EventStripeNode.this); } @@ -566,9 +370,6 @@ final public class EventStripeNode extends StackPane { contextMenu = new ContextMenu(); contextMenu.setAutoHide(true); - contextMenu.getItems().add(ActionUtils.createMenuItem(new ExpandClusterAction())); - contextMenu.getItems().add(ActionUtils.createMenuItem(new CollapseClusterAction())); - contextMenu.getItems().add(new SeparatorMenuItem()); contextMenu.getItems().addAll(chartContextMenu.getItems()); } @@ -577,39 +378,4 @@ final public class EventStripeNode extends StackPane { } } } - - private class ExpandClusterAction extends Action { - - @NbBundle.Messages("ExpandClusterAction.text=Expand") - ExpandClusterAction() { - super(Bundle.ExpandClusterAction_text()); - - setGraphic(new ImageView(PLUS)); - setEventHandler((ActionEvent t) -> { - final DescriptionLoD next = descLOD.get().moreDetailed(); - if (next != null) { - loadSubBundles(DescriptionLoD.RelativeDetail.MORE); - - } - }); - disabledProperty().bind(descLOD.isEqualTo(DescriptionLoD.FULL)); - } - } - - private class CollapseClusterAction extends Action { - - @NbBundle.Messages("CollapseClusterAction.text=Collapse") - CollapseClusterAction() { - super(Bundle.CollapseClusterAction_text()); - - setGraphic(new ImageView(MINUS)); - setEventHandler((ActionEvent t) -> { - final DescriptionLoD previous = descLOD.get().lessDetailed(); - if (previous != null) { - loadSubBundles(DescriptionLoD.RelativeDetail.LESS); - } - }); - disabledProperty().bind(Bindings.createBooleanBinding(() -> nonNull(eventStripe) && descLOD.get() == eventStripe.getDescriptionLoD(), descLOD)); - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java index b3e80eb827..6feef28f8f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -35,13 +35,13 @@ class EventDescriptionTreeItem extends NavTreeItem { * maps a description to the child item of this item with that description */ private final Map childMap = new ConcurrentHashMap<>(); - private final EventBundle bundle; + private final EventBundle bundle; - public EventBundle getEventBundle() { + public EventBundle getEventBundle() { return bundle; } - EventDescriptionTreeItem(EventBundle g) { + EventDescriptionTreeItem(EventBundle g) { bundle = g; setValue(g); } @@ -51,8 +51,8 @@ class EventDescriptionTreeItem extends NavTreeItem { return getValue().getCount(); } - public void insert(Deque path) { - EventBundle head = path.removeFirst(); + public void insert(Deque> path) { + EventBundle head = path.removeFirst(); EventDescriptionTreeItem treeItem = childMap.get(head.getDescription()); if (treeItem == null) { treeItem = new EventDescriptionTreeItem(head); @@ -68,12 +68,12 @@ class EventDescriptionTreeItem extends NavTreeItem { } @Override - public void resort(Comparator> comp) { + public void resort(Comparator>> comp) { FXCollections.sort(getChildren(), comp); } @Override - public NavTreeItem findTreeItemForEvent(EventBundle t) { + public NavTreeItem findTreeItemForEvent(EventBundle t) { if (getValue().getEventType() == t.getEventType() && getValue().getDescription().equals(t.getDescription())) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java index f1b91bf202..e921991191 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -33,9 +33,9 @@ class EventTypeTreeItem extends NavTreeItem { */ private final Map childMap = new ConcurrentHashMap<>(); - private final Comparator> comparator = TreeComparator.Description; + private final Comparator>> comparator = TreeComparator.Description; - EventTypeTreeItem(EventBundle g) { + EventTypeTreeItem(EventBundle g) { setValue(g); } @@ -44,8 +44,8 @@ class EventTypeTreeItem extends NavTreeItem { return getValue().getCount(); } - public void insert(Deque path) { - EventBundle head = path.removeFirst(); + public void insert(Deque> path) { + EventBundle head = path.removeFirst(); EventDescriptionTreeItem treeItem = childMap.get(head.getDescription()); if (treeItem == null) { treeItem = new EventDescriptionTreeItem(head); @@ -61,7 +61,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public NavTreeItem findTreeItemForEvent(EventBundle t) { + public NavTreeItem findTreeItemForEvent(EventBundle t) { if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) { for (EventDescriptionTreeItem child : childMap.values()) { @@ -75,7 +75,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public void resort(Comparator> comp) { + public void resort(Comparator>> comp) { FXCollections.sort(getChildren(), comp); } } 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 9b8378ed6b..7b96a0ea53 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 @@ -68,13 +68,13 @@ public class NavPanel extends BorderPane implements TimeLineView { private DetailViewPane detailViewPane; @FXML - private TreeView eventsTree; + private TreeView> eventsTree; @FXML private Label eventsTreeLabel; @FXML - private ComboBox>> sortByBox; + private ComboBox>>> sortByBox; public NavPanel() { FXMLConstructor.construct(this, "NavPanel.fxml"); // NON-NLS @@ -91,8 +91,8 @@ public class NavPanel extends BorderPane implements TimeLineView { detailViewPane.getSelectedNodes().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); - detailViewPane.getSelectedNodes().forEach(eventStripeNode -> { - eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventStripeNode.getEventStripe())); + detailViewPane.getSelectedNodes().forEach(eventBundleNode -> { + eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventBundleNode.getEventBundle())); }); }); @@ -105,7 +105,7 @@ public class NavPanel extends BorderPane implements TimeLineView { @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void setRoot() { RootItem root = new RootItem(); - for (EventBundle bundle : detailViewPane.getEventBundles()) { + for (EventBundle bundle : detailViewPane.getEventBundles()) { root.insert(bundle); } eventsTree.setRoot(root); @@ -134,7 +134,7 @@ public class NavPanel extends BorderPane implements TimeLineView { getRoot().resort(sortByBox.getSelectionModel().getSelectedItem()); }); eventsTree.setShowRoot(false); - eventsTree.setCellFactory((TreeView p) -> new EventBundleTreeCell()); + eventsTree.setCellFactory((TreeView> p) -> new EventBundleTreeCell()); eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); eventsTreeLabel.setText(NbBundle.getMessage(this.getClass(), "NavPanel.eventsTreeLabel.text")); @@ -144,7 +144,7 @@ public class NavPanel extends BorderPane implements TimeLineView { * A tree cell to display {@link EventBundle}s. Shows the description, and * count, as well a a "legend icon" for the event type. */ - private class EventBundleTreeCell extends TreeCell { + private class EventBundleTreeCell extends TreeCell> { private static final double HIDDEN_MULTIPLIER = .6; private final Rectangle rect = new Rectangle(24, 24); @@ -158,7 +158,7 @@ public class NavPanel extends BorderPane implements TimeLineView { } @Override - protected void updateItem(EventBundle item, boolean empty) { + protected void updateItem(EventBundle item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { setText(null); @@ -177,7 +177,7 @@ public class NavPanel extends BorderPane implements TimeLineView { }); registerListeners(controller.getQuickHideFilters(), item); String text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS - TreeItem parent = getTreeItem().getParent(); + TreeItem> parent = getTreeItem().getParent(); if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) { text = StringUtils.substringAfter(text, parent.getValue().getDescription()); } @@ -189,7 +189,7 @@ public class NavPanel extends BorderPane implements TimeLineView { } } - private void registerListeners(Collection filters, EventBundle item) { + private void registerListeners(Collection filters, EventBundle item) { for (DescriptionFilter filter : filters) { if (filter.getDescription().equals(item.getDescription())) { filter.activeProperty().addListener(filterStateChangeListener); @@ -205,8 +205,8 @@ public class NavPanel extends BorderPane implements TimeLineView { } } - private void updateHiddenState(EventBundle item) { - TreeItem treeItem = getTreeItem(); + private void updateHiddenState(EventBundle item) { + TreeItem> treeItem = getTreeItem(); ContextMenu newMenu; if (controller.getQuickHideFilters().stream(). filter(AbstractFilter::isActive) 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 d1df7a5b38..bed99d2270 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 @@ -28,11 +28,11 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; * {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type, * description , count, etc. */ -abstract class NavTreeItem extends TreeItem { +abstract class NavTreeItem extends TreeItem> { abstract long getCount(); - abstract void resort(Comparator> comp); + abstract void resort(Comparator>> comp); - abstract NavTreeItem findTreeItemForEvent(EventBundle t); + abstract NavTreeItem findTreeItemForEvent(EventBundle t); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java index a96dfaae20..571758a037 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -56,7 +56,7 @@ class RootItem extends NavTreeItem { * * @param g Group to add */ - public void insert(EventBundle g) { + public void insert(EventBundle g) { EventTypeTreeItem treeItem = childMap.computeIfAbsent(g.getEventType().getBaseType(), baseType -> { @@ -69,12 +69,12 @@ class RootItem extends NavTreeItem { treeItem.insert(getTreePath(g)); } - static Deque getTreePath(EventBundle g) { - Deque path = new ArrayDeque<>(); - Optional p = Optional.of(g); + static Deque< EventBundle> getTreePath(EventBundle g) { + Deque> path = new ArrayDeque<>(); + Optional> p = Optional.of(g); while (p.isPresent()) { - EventBundle parent = p.get(); + EventBundle parent = p.get(); path.addFirst(parent); p = parent.getParentBundle(); } @@ -83,12 +83,12 @@ class RootItem extends NavTreeItem { } @Override - public void resort(Comparator> comp) { + public void resort(Comparator>> comp) { childMap.values().forEach(ti -> ti.resort(comp)); } @Override - public NavTreeItem findTreeItemForEvent(EventBundle t) { + public NavTreeItem findTreeItemForEvent(EventBundle t) { for (EventTypeTreeItem child : childMap.values()) { final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); if (findTreeItemForEvent != null) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java index aad194f61c..195a286ed9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java @@ -23,23 +23,23 @@ import javafx.scene.control.TreeItem; import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -enum TreeComparator implements Comparator> { +enum TreeComparator implements Comparator>> { Description { @Override - public int compare(TreeItem o1, TreeItem o2) { + public int compare(TreeItem> o1, TreeItem> o2) { return o1.getValue().getDescription().compareTo(o2.getValue().getDescription()); } }, Count { @Override - public int compare(TreeItem o1, TreeItem o2) { + public int compare(TreeItem> o1, TreeItem> o2) { return Long.compare(o2.getValue().getCount(), o1.getValue().getCount()); } }, Type { @Override - public int compare(TreeItem o1, TreeItem o2) { + public int compare(TreeItem> o1, TreeItem> o2) { return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType()); } }; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java index 506554fbb7..3d55d7a702 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java @@ -164,7 +164,7 @@ final public class FilterSetPanel extends BorderPane implements TimeLineView { //configure tree column to show name of filter and checkbox treeColumn.setCellValueFactory(param -> param.getValue().valueProperty()); - treeColumn.setCellFactory(col -> new FilterCheckBoxCellFactory().forTreeTable(col)); + treeColumn.setCellFactory(col -> new FilterCheckBoxCellFactory<>().forTreeTable(col)); //configure legend column to show legend (or othe supplamantal ui, eg, text field for text filter) legendColumn.setCellValueFactory(param -> param.getValue().valueProperty()); From f74859c47c1865a1409ee0e31530a87e3e47c424 Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 5 Oct 2015 17:08:35 -0400 Subject: [PATCH 02/13] refactor layout across two parts of hierarchy (stripes and clusters) --- .../timeline/datamodel/EventBundle.java | 4 +- .../timeline/datamodel/EventCluster.java | 8 - .../timeline/datamodel/EventStripe.java | 2 +- .../ui/detailview/EventBundleNodeBase.java | 55 +-- .../ui/detailview/EventClusterNode.java | 164 +++++--- .../ui/detailview/EventDetailChart.java | 372 ++++-------------- .../ui/detailview/EventStripeNode.java | 55 ++- 7 files changed, 231 insertions(+), 429 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java index 070fa027dd..dd088b8420 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java @@ -27,7 +27,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * */ -public interface EventBundle> { +public interface EventBundle> { String getDescription(); @@ -45,7 +45,7 @@ public interface EventBundle> { long getStartMillis(); - Optional getParentBundle(); + Optional getParentBundle(); default long getCount() { return getEventIDs().size(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index a76bda8536..592c4674f6 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -168,14 +168,6 @@ public class EventCluster implements EventBundle { return lod; } -// Range getRange() { -// if (getEndMillis() > getStartMillis()) { -// return Range.closedOpen(getSpan().getStartMillis(), getSpan().getEndMillis()); -// } else { -// return Range.singleton(getStartMillis()); -// } -// } - /** * return a new EventCluster identical to this one, except with the given * EventBundle as the parent. diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index 86b822bc2c..6c4a6fa1f7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -107,7 +107,7 @@ public final class EventStripe implements EventBundle { tagged.addAll(v.getEventIDsWithTags()); hashHits.addAll(u.getEventIDsWithHashHits()); hashHits.addAll(v.getEventIDsWithHashHits()); - parent = u.getParentBundle().orElse(null); + parent = u.getParentBundle().orElse(v.getParentBundle().orElse(null)); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java index b9ef1324a7..bf9806e2d0 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -18,7 +18,7 @@ import javafx.scene.layout.CornerRadii; import javafx.scene.layout.Pane; import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; import static javafx.scene.layout.Region.USE_PREF_SIZE; -import javafx.scene.layout.VBox; +import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import org.controlsfx.control.action.ActionUtils; import org.joda.time.DateTime; @@ -32,7 +32,7 @@ import org.sleuthkit.datamodel.SleuthkitCase; /** * */ -public abstract class EventBundleNodeBase, ParentBundleType extends EventBundle, ParentNodeType extends EventBundleNodeBase> extends VBox { +public abstract class EventBundleNodeBase, ParentType extends EventBundle, ParentNodeType extends EventBundleNodeBase> extends StackPane { static final CornerRadii CORNER_RADII_3 = new CornerRadii(3); static final CornerRadii CORNER_RADII_1 = new CornerRadii(1); @@ -48,8 +48,6 @@ public abstract class EventBundleNodeBase, Par final Background highlightedBackground; final Background defaultBackground; final Color evtColor; -// final Button plusButton = ActionUtils.createButton(new EventStripeNode.ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE); -// final Button minusButton = ActionUtils.createButton(new EventStripeNode.CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE); final Button hideButton; final List subNodes = new ArrayList<>(); final Pane subNodePane = new Pane(); @@ -66,12 +64,9 @@ public abstract class EventBundleNodeBase, Par defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY)); highlightedBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY)); - setBackground(defaultBackground); EventDetailChart.HideDescriptionAction hideClusterAction = chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD()); hideButton = ActionUtils.createButton(hideClusterAction, ActionUtils.ActionTextBehavior.HIDE); configureLoDButton(hideButton); -// configureLoDButton(plusButton); -// configureLoDButton(minusButton); setAlignment(Pos.TOP_LEFT); setMinHeight(24); @@ -79,6 +74,14 @@ public abstract class EventBundleNodeBase, Par setMaxHeight(USE_PREF_SIZE); setMaxWidth(USE_PREF_SIZE); setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()); + + //set up subnode pane sizing contraints + subNodePane.setPrefHeight(USE_COMPUTED_SIZE); + subNodePane.setMinHeight(24); + subNodePane.setMaxHeight(USE_PREF_SIZE); + subNodePane.setPrefWidth(USE_COMPUTED_SIZE); + subNodePane.setMinWidth(USE_PREF_SIZE); + subNodePane.setMaxWidth(USE_PREF_SIZE); } final DescriptionLoD getDescriptionLoD() { @@ -94,22 +97,6 @@ public abstract class EventBundleNodeBase, Par + getBoundsInParent().getMinX(); } - final EventType getEventType() { - return getEventBundle().getEventType(); - } - - final String getDescription() { - return getEventBundle().getDescription(); - } - - final long getStartMillis() { - return getEventBundle().getStartMillis(); - } - - final Set getEventsIDs() { - return getEventBundle().getEventIDs(); - } - abstract void installTooltip(); abstract void applySelectionEffect(boolean selected); @@ -127,4 +114,26 @@ public abstract class EventBundleNodeBase, Par } abstract void setDescriptionVisibility(DescriptionVisibility get); + + final EventType getEventType() { + return getEventBundle().getEventType(); + } + + final String getDescription() { + return getEventBundle().getDescription(); + } + + final long getStartMillis() { + return getEventBundle().getStartMillis(); + } + + final long getEndMillis() { + return getEventBundle().getEndMillis(); + } + + final Set getEventsIDs() { + return getEventBundle().getEventIDs(); + } + + abstract void layoutChildren(double xOffset); } 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 7d6c805633..83145036c9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -16,29 +16,34 @@ import javafx.beans.binding.Bindings; import javafx.concurrent.Task; import javafx.event.ActionEvent; 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.SeparatorMenuItem; 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.HBox; +import javafx.scene.layout.Region; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionUtils; +import org.joda.time.DateTime; import org.joda.time.Interval; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.CORNER_RADII_3; +import org.sleuthkit.autopsy.timeline.filters.TypeFilter; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventStripeNode.configureLoDButton; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventStripeNode.show; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; @@ -47,18 +52,24 @@ import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; * */ public class EventClusterNode extends EventBundleNodeBase { - + private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1); private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); - + + private final Region clusterRegion = new Region(); + + final Button plusButton = ActionUtils.createButton(new ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE); + final Button minusButton = ActionUtils.createButton(new CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE); + public EventClusterNode(EventDetailChart chart, EventCluster eventCluster, EventStripeNode parentNode) { super(chart, eventCluster, parentNode); - - - setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII_3, Insets.EMPTY))); - setBorder(clusterBorder); - + + clusterRegion.setBorder(clusterBorder); + clusterRegion.setBackground(defaultBackground); + clusterRegion.setMaxWidth(USE_PREF_SIZE); + clusterRegion.setMinWidth(USE_PREF_SIZE); + setMinHeight(24); setCursor(Cursor.HAND); setOnMouseClicked(new MouseClickHandler()); @@ -69,39 +80,51 @@ public class EventClusterNode extends EventBundleNodeBase { -// showDescriptionLoDControls(false); + showDescriptionLoDControls(false); }); - + + configureLoDButton(plusButton); + configureLoDButton(minusButton); + HBox buttonBar = new HBox(5, plusButton, minusButton); + buttonBar.setMaxWidth(USE_PREF_SIZE); + buttonBar.setAlignment(Pos.BOTTOM_LEFT); + + getChildren().addAll(clusterRegion, subNodePane, buttonBar); } - + + void showDescriptionLoDControls(final boolean showControls) { + show(minusButton, showControls); + show(plusButton, showControls); + } + @Override final void installTooltip() { - + } - + @Override void applySelectionEffect(boolean selected) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - + @Override void applyHighlightEffect(boolean applied) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - + void setSpanWidths(List spanWidths) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - + void setDescriptionWidth(double max) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - + @Override void setDescriptionVisibility(DescriptionVisibility get) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. @@ -111,12 +134,12 @@ public class EventClusterNode extends EventBundleNodeBase { - + private ContextMenu contextMenu; - + @Override public void handle(MouseEvent t) { - + if (t.getButton() == MouseButton.PRIMARY) { t.consume(); if (t.isShiftDown()) { @@ -129,7 +152,7 @@ public class EventClusterNode extends EventBundleNodeBase - getSubNodes().stream().anyMatch(subNode -> + subNodes.stream().anyMatch(subNode -> bundle.equals(subNode.getEventStripe())) ); subNodePane.getChildren().clear(); + subNodes.clear(); if (descLOD.get().withRelativeDetail(relativeDetail) == getEventBundle().getDescriptionLoD()) { descLOD.set(getEventBundle().getDescriptionLoD()); -// clustersHBox.setVisible(true); chart.setRequiresLayout(true); chart.requestChartLayout(); } else { -// clustersHBox.setVisible(false); - - // make new ZoomParams to query with - final RootFilter subClusterFilter = null;//getSubClusterFilter(); /* + * make new ZoomParams to query with + * + * * We need to extend end time because for the query by one second, * because it is treated as an open interval but we want to include * events at exactly the time of the last event in this cluster */ - final Interval subClusterSpan = new Interval(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000); + final RootFilter subClusterFilter = getSubClusterFilter(); + final Interval subClusterSpan = new Interval(getStartMillis(), getEndMillis() + 1000); final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); - + Task> loggedTask = new Task>() { - + private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); - + { updateTitle(Bundle.EventStripeNode_loggedTask_name()); } - + @Override protected Collection call() throws Exception { Collection bundles; @@ -203,7 +226,6 @@ public class EventClusterNode extends EventBundleNodeBase cluster.withParent(getEventBundle())) .collect(Collectors.toMap((eventCluster) -> eventCluster.getDescription(), //key (eventCluster) -> new EventStripe(eventCluster, getEventCluster()), //value EventStripe::merge) //merge method @@ -213,32 +235,31 @@ public class EventClusterNode extends EventBundleNodeBase bundles = get(); - + if (bundles.isEmpty()) { -// clustersHBox.setVisible(true); } else { -// clustersHBox.setVisible(false); chart.getEventBundles().addAll(bundles); - subNodePane.getChildren().setAll(bundles.stream() + subNodes.addAll(bundles.stream() .map(EventClusterNode.this::getSubNodeForBundle) .collect(Collectors.toSet())); + subNodePane.getChildren().addAll(subNodes); } descLOD.set(loadedDescriptionLoD); //assign subNodes and request chart layout - chart.setRequiresLayout(true); - chart.requestChartLayout(); } catch (InterruptedException | ExecutionException ex) { LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); } + chart.setRequiresLayout(true); + chart.requestChartLayout(); chart.setCursor(null); } }; @@ -247,38 +268,38 @@ public class EventClusterNode extends EventBundleNodeBase { final DescriptionLoD next = descLOD.get().moreDetailed(); if (next != null) { loadSubBundles(DescriptionLoD.RelativeDetail.MORE); - + } }); disabledProperty().bind(descLOD.isEqualTo(DescriptionLoD.FULL)); } } - + private class CollapseClusterAction extends Action { - + @NbBundle.Messages("CollapseClusterAction.text=Collapse") CollapseClusterAction() { super(Bundle.CollapseClusterAction_text()); - + setGraphic(new ImageView(MINUS)); setEventHandler((ActionEvent t) -> { final DescriptionLoD previous = descLOD.get().lessDetailed(); @@ -289,8 +310,35 @@ public class EventClusterNode extends EventBundleNodeBase nonNull(getEventCluster()) && descLOD.get() == getEventCluster().getDescriptionLoD(), descLOD)); } } - + EventCluster getEventCluster() { return getEventBundle(); } + + @Override + protected void layoutChildren(double xOffset) { + super.layoutChildren(); //To change body of generated methods, choose Tools | Templates. + double chartX = chart.getXAxis().getDisplayPosition(new DateTime(getStartMillis())); + double startX = chartX - xOffset; + double w = chart.getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX; + + //position of start and end according to range of axis + setLayoutX(startX); + clusterRegion.setPrefWidth(w); + chart.layoutStripes(subNodes, 0, chartX); + autosize(); + } + + /** + * make a new filter intersecting the global filter with description and + * type filters to restrict sub-clusters + * + */ + RootFilter getSubClusterFilter() { + RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf(); + subClusterFilter.getSubFilters().addAll( + new DescriptionFilter(getDescriptionLoD(), getDescription(), DescriptionFilter.FilterMode.INCLUDE), + new TypeFilter(getEventType())); + return subClusterFilter; + } } 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 e87ecc2a59..1b31e6c76d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -18,13 +18,11 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.MissingResourceException; @@ -164,9 +162,9 @@ public final class EventDetailChart extends XYChart impl FXCollections.>observableArrayList(); private final ObservableList> sortedSeriesList = seriesList - .sorted((s1, s2) -> { - final List collect = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList()); - return Integer.compare(collect.indexOf(s1.getName()), collect.indexOf(s2.getName())); + .sorted((Series s1, Series s2) -> { + final List eventTypeNames = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList()); + return Integer.compare(eventTypeNames.indexOf(s1.getName()), eventTypeNames.indexOf(s2.getName())); }); /** * true == layout each event type in its own band, false == mix all the @@ -192,13 +190,13 @@ public final class EventDetailChart extends XYChart impl * the labels, alow them to extend past the timespan indicator and off the * edge of the screen */ - private final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false); + final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false); /** * the width to truncate all labels to if truncateAll is true. adjustable * via slider if truncateAll is true */ - private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); + final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true); EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { @@ -258,7 +256,6 @@ public final class EventDetailChart extends XYChart impl while (change.next()) { change.getRemoved().forEach((EventBundleNodeBase removedNode) -> { removedNode.getEventBundle().getClusters().forEach(cluster -> { - Line removedLine = projectionMap.remove(cluster); getChartChildren().removeAll(removedLine); }); @@ -412,7 +409,6 @@ public final class EventDetailChart extends XYChart impl stripeNodeMap.put(eventStripe, stripeNode); nodeGroup.getChildren().add(stripeNode); data.setNode(stripeNode); - } @Override @@ -428,7 +424,6 @@ public final class EventDetailChart extends XYChart impl EventStripe removedStripe = stripeDescMap.remove(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription())); EventStripeNode removedNode = stripeNodeMap.remove(removedStripe); nodeGroup.getChildren().remove(removedNode); - data.setNode(null); } @@ -470,26 +465,23 @@ public final class EventDetailChart extends XYChart impl if (requiresLayout) { setCursor(Cursor.WAIT); - maxY.set(0.0); - - Map> hiddenPartition; + //hash map from y value to right most occupied x value. This tells you for a given 'pixel row' what is the first avaialable slot + maxY.set(0); + List stripeNodes; if (bandByType.get()) { - double minY = 0; for (Series series : sortedSeriesList) { - hiddenPartition = series.getData().stream().map(Data::getNode).map(EventStripeNode.class::cast) - .collect(Collectors.partitioningBy(node -> getController().getQuickHideFilters().stream() - .filter(AbstractFilter::isActive) - .anyMatch(filter -> filter.getDescription().equals(node.getDescription())))); + stripeNodes = series.getData().stream() + .map(data -> (EventStripeNode) data.getNode()) + .sorted(Comparator.comparing(EventStripeNode::getStartMillis)). + collect(Collectors.toList()); + maxY.set(maxY.get() + layoutStripes(stripeNodes, maxY.get(), 0)); - layoutStripeNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, 0); - minY = maxY.get(); } } else { - hiddenPartition = stripeNodeMap.values().stream() - .collect(Collectors.partitioningBy(node -> getController().getQuickHideFilters().stream() - .filter(AbstractFilter::isActive) - .anyMatch(filter -> filter.getDescription().equals(node.getDescription())))); - layoutStripeNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), 0, 0); + stripeNodes = stripeNodeMap.values().stream() + .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) + .collect(Collectors.toList()); + maxY.set(layoutStripes(stripeNodes, 0, 0)); } setCursor(null); requiresLayout = false; @@ -497,66 +489,6 @@ public final class EventDetailChart extends XYChart impl layoutProjectionMap(); } - /** - * - * @param hiddenNodes the value of hiddenNodes - * @param shownNodes the value of shownNodes - * @param minY the value of minY - * @param children the value of children - * @param xOffset the value of xOffset - * - * @return the double - */ - private double layoutStripeNodesHelper(List hiddenNodes, List shownNodes, double minY, final double xOffset) { - - hiddenNodes.forEach((EventStripeNode t) -> { -// children.remove(t); - t.setVisible(false); - t.setManaged(false); - }); - - shownNodes.forEach((EventStripeNode t) -> { -// if (false == children.contains(t)) { -// children.add(t); -// } - t.setVisible(true); - t.setManaged(true); - }); - - shownNodes.sort(Comparator.comparing(EventStripeNode::getStartMillis)); - return layoutStripeNodes(shownNodes, minY, xOffset); - } - - /** - * - * @param hiddenNodes the value of hiddenNodes - * @param shownNodes the value of shownNodes - * @param minY the value of minY - * @param children the value of children - * @param xOffset the value of xOffset - * - * @return the double - */ - private double layoutClusterNodesHelper(List hiddenNodes, List shownNodes, double minY, final double xOffset) { - - hiddenNodes.forEach((EventClusterNode t) -> { -// children.remove(t); - t.setVisible(false); - t.setManaged(false); - }); - - shownNodes.forEach((EventClusterNode t) -> { -// if (false == children.contains(t)) { -// children.add(t); -// } - t.setVisible(true); - t.setManaged(true); - }); - - shownNodes.sort(Comparator.comparing(EventClusterNode::getStartMillis)); - return layoutClusterNodes(shownNodes, minY, xOffset); - } - @Override protected synchronized void seriesAdded(Series series, int i) { for (int j = 0; j < series.getData().size(); j++) { @@ -627,237 +559,69 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - private synchronized double layoutStripeNodes(final Collection< EventStripeNode> nodes, final double minY, final double xOffset) { - //hash map from y value to right most occupied x value. This tells you for a given 'pixel row' what is the first avaialable slot - Map maxXatY = new HashMap<>(); + synchronized double layoutStripes(final Collection nodes, final double minY, final double xOffset) { + final Map maxXatY = new HashMap<>(); double localMax = minY; //for each node size it and position it in first available slot - for (EventStripeNode stripeNode : nodes) { - - stripeNode.setDescriptionVisibility(descrVisibility.get()); - double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis())); - - //position of start and end according to range of axis - double startX = rawDisplayPosition - xOffset; - double layoutNodesResultHeight = 0; - double span = 0; - - List spanWidths = new ArrayList<>(); - double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));; - double x2; - Iterator ranges = stripeNode.getEventStripe().getClusters().iterator(); - EventCluster cluster = ranges.next(); - do { - x2 = getXAxis().getDisplayPosition(new DateTime(cluster.getEndMillis())); - double clusterSpan = x2 - x; - span += clusterSpan; - spanWidths.add(clusterSpan); - if (ranges.hasNext()) { - cluster = ranges.next(); - x = getXAxis().getDisplayPosition(new DateTime(cluster.getStartMillis())); - double gapSpan = x - x2; - span += gapSpan; - spanWidths.add(gapSpan); - if (ranges.hasNext() == false) { - x2 = getXAxis().getDisplayPosition(new DateTime(cluster.getEndMillis())); - clusterSpan = x2 - x; - span += clusterSpan; - spanWidths.add(clusterSpan); - } - } - - } while (ranges.hasNext()); - - stripeNode.setSpanWidths(spanWidths); - - if (truncateAll.get()) { //if truncate option is selected limit width of description label - stripeNode.setDescriptionWidth(Math.max(span, truncateWidth.get())); - } else { //else set it unbounded - stripeNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); - } - List subNodes = stripeNode.getSubNodes(); - if (subNodes.isEmpty() == false) { - Map> hiddenPartition = subNodes.stream() - .collect(Collectors.partitioningBy(testNode -> getController().getQuickHideFilters().stream() - .filter(AbstractFilter::isActive) - .anyMatch(filter -> filter.getDescription().equals(testNode.getDescription())))); - - layoutNodesResultHeight = layoutClusterNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, rawDisplayPosition); - } - - stripeNode.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 + stripeNode.getWidth(); - - //get the height of the node - final double h = layoutNodesResultHeight == 0 ? stripeNode.getHeight() : layoutNodesResultHeight ; - - //initial test position - double yPos = minY; - double yPos2 = yPos + h; - - if (oneEventPerRow.get()) { - // if onePerRow, just put it at end - yPos = (localMax + 2); - yPos2 = yPos + h; - + for (EventBundleNodeBase stripeNode : nodes) { + boolean quickHide = getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(stripeNode.getDescription())); + if (quickHide) { + stripeNode.setVisible(false); + stripeNode.setManaged(false); } else { - boolean overlapping = true; - while (overlapping) { - //loop through y values looking for available slot. + stripeNode.setVisible(true); + stripeNode.setManaged(true); + stripeNode.setDescriptionVisibility(descrVisibility.get()); + stripeNode.layoutChildren(xOffset); + double xLeft = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis())) - xOffset; + double xRight = xLeft + stripeNode.getBoundsInParent().getWidth(); - overlapping = false; - //check each pixel from bottom to top. - for (double y = yPos2; y >= yPos; y--) { - final Double maxX = maxXatY.get((int) y); - if (maxX != null && maxX >= startX - 4) { - //if that pixel is already used - //jump top to this y value and repeat until free slot is found. - overlapping = true; - yPos = y + 4; - yPos2 = yPos + h; - break; + //get the height of the node + final double h = stripeNode.getBoundsInParent().getHeight(); + + //initial test position + double yTop = minY; + double yBottom = yTop + h; + + if (oneEventPerRow.get()) { + // if onePerRow, just put it at end + yTop = (localMax + 2); + yBottom = yTop + h; + } else { + boolean overlapping = true; + while (overlapping) { + //loop through y values looking for available slot. + overlapping = false; + //check each pixel from bottom to top. + for (double y = yBottom; y >= yTop; y--) { + final Double maxX = maxXatY.get((int) y); + if (maxX != null && maxX >= xLeft - 4) { + //if that pixel is already used + //jump top to this y value and repeat until free slot is found. + overlapping = true; + yTop = y + 4; + yBottom = yTop + h; + break; + } } } - } - //mark used y values - for (double y = yPos; y <= yPos2; y++) { - maxXatY.put((int) y, xRight); - } - } - localMax = Math.max(yPos2, localMax); - - Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), - new KeyValue(stripeNode.layoutXProperty(), startX), - new KeyValue(stripeNode.layoutYProperty(), yPos))); - - tm.play(); - } - maxY.set(Math.max(maxY.get(), localMax)); - return localMax - minY; - } - - /** - * layout the nodes in the given list, starting form the given minimum y - * coordinate. - * - * @param nodes - * @param minY - */ - private synchronized double layoutClusterNodes(final Collection< EventClusterNode> 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 (EventClusterNode clusterNode : nodes) { - - clusterNode.setDescriptionVisibility(descrVisibility.get()); - double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(clusterNode.getStartMillis())); - - //position of start and end according to range of axis - double startX = rawDisplayPosition - xOffset; - double layoutNodesResultHeight = 0; - - double span = 0; - - List spanWidths = new ArrayList<>(); - double x = getXAxis().getDisplayPosition(new DateTime(clusterNode.getStartMillis()));; - double x2; - Iterator ranges = clusterNode.getEventCluster().getClusters().iterator(); - EventCluster range = ranges.next(); - do { - x2 = getXAxis().getDisplayPosition(new DateTime(range.getEndMillis())); - double clusterSpan = x2 - x; - span += clusterSpan; - spanWidths.add(clusterSpan); - if (ranges.hasNext()) { - range = ranges.next(); - x = getXAxis().getDisplayPosition(new DateTime(range.getStartMillis())); - double gapSpan = x - x2; - span += gapSpan; - spanWidths.add(gapSpan); - if (ranges.hasNext() == false) { - x2 = getXAxis().getDisplayPosition(new DateTime(range.getEndMillis())); - clusterSpan = x2 - x; - span += clusterSpan; - spanWidths.add(clusterSpan); + //mark used y values + for (double y = yTop; y <= yBottom; y++) { + maxXatY.put((int) y, xRight); } } + localMax = Math.max(yBottom, localMax); - } while (ranges.hasNext()); + Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), + new KeyValue(stripeNode.layoutXProperty(), xLeft), + new KeyValue(stripeNode.layoutYProperty(), yTop))); - clusterNode.setSpanWidths(spanWidths); - - if (truncateAll.get()) { //if truncate option is selected limit width of description label - clusterNode.setDescriptionWidth(Math.max(span, truncateWidth.get())); - } else { //else set it unbounded - clusterNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); + tm.play(); } - - List subNodes = clusterNode.getSubNodes(); - if (subNodes.isEmpty() == false) { - Map> hiddenPartition = subNodes.stream() - .collect(Collectors.partitioningBy(testNode -> getController().getQuickHideFilters().stream() - .filter(AbstractFilter::isActive) - .anyMatch(filter -> filter.getDescription().equals(testNode.getDescription())))); - - layoutNodesResultHeight = layoutStripeNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, rawDisplayPosition); - } - - clusterNode.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 + clusterNode.getWidth(); - - //get the height of the node - final double h = layoutNodesResultHeight == 0 ? clusterNode.getHeight() : layoutNodesResultHeight ; - //initial test position - double yPos = minY; - - double yPos2 = yPos + h; - - if (oneEventPerRow.get()) { - // if onePerRow, just put it at end - yPos = (localMax + 2); - yPos2 = yPos + h; - - } else {//else - - boolean overlapping = true; - while (overlapping) { - //loop through y values looking for available slot. - - overlapping = false; - //check each pixel from top to bottom. - for (double y = yPos2; y >= yPos; y--) { - final Double maxX = maxXatY.get((int) y); - if (maxX != null && maxX >= startX - 4) { - //if that pixel is already used - //jump top to this y value and repeat until free slot is found. - overlapping = true; - yPos = y + 4; - yPos2 = yPos + h; - break; - } - } - } - //mark used y values - for (double y = yPos; y <= yPos2; y++) { - maxXatY.put((int) y, xRight); - } - } - localMax = Math.max(yPos2, localMax); - - Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), - new KeyValue(clusterNode.layoutXProperty(), startX), - new KeyValue(clusterNode.layoutYProperty(), yPos))); - - tm.play(); } - maxY.set(Math.max(maxY.get(), localMax)); - return localMax - minY; + return localMax; } private void layoutProjectionMap() { 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 3cd77b26e9..5e0bb3a13f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -52,6 +52,7 @@ import javafx.scene.layout.Region; import static javafx.scene.layout.Region.USE_PREF_SIZE; import javafx.scene.paint.Color; import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; @@ -59,9 +60,6 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; -import org.sleuthkit.autopsy.timeline.filters.RootFilter; -import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.datamodel.TskCoreException; /** @@ -110,7 +108,7 @@ final public class EventStripeNode extends EventBundleNodeBase { @@ -270,19 +258,6 @@ final public class EventStripeNode extends EventBundleNodeBase Date: Tue, 6 Oct 2015 10:07:19 -0400 Subject: [PATCH 03/13] minor cleanup --- .../ui/detailview/EventClusterNode.java | 84 +++++++++---------- .../ui/detailview/EventDetailChart.java | 7 +- 2 files changed, 46 insertions(+), 45 deletions(-) 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 83145036c9..5af019d2e9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -51,24 +51,24 @@ import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; /** * */ -public class EventClusterNode extends EventBundleNodeBase { - +final public class EventClusterNode extends EventBundleNodeBase { + private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1); private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); - + private final Region clusterRegion = new Region(); - + final Button plusButton = ActionUtils.createButton(new ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE); final Button minusButton = ActionUtils.createButton(new CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE); - + public EventClusterNode(EventDetailChart chart, EventCluster eventCluster, EventStripeNode parentNode) { super(chart, eventCluster, parentNode); - + clusterRegion.setBorder(clusterBorder); clusterRegion.setBackground(defaultBackground); clusterRegion.setMaxWidth(USE_PREF_SIZE); - clusterRegion.setMinWidth(USE_PREF_SIZE); + clusterRegion.setMinWidth(1); setMinHeight(24); setCursor(Cursor.HAND); setOnMouseClicked(new MouseClickHandler()); @@ -83,48 +83,48 @@ public class EventClusterNode extends EventBundleNodeBase { showDescriptionLoDControls(false); }); - + configureLoDButton(plusButton); configureLoDButton(minusButton); HBox buttonBar = new HBox(5, plusButton, minusButton); buttonBar.setMaxWidth(USE_PREF_SIZE); buttonBar.setAlignment(Pos.BOTTOM_LEFT); - + getChildren().addAll(clusterRegion, subNodePane, buttonBar); } - + void showDescriptionLoDControls(final boolean showControls) { show(minusButton, showControls); show(plusButton, showControls); } - + @Override - final void installTooltip() { - + void installTooltip() { + } - + @Override void applySelectionEffect(boolean selected) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - + @Override void applyHighlightEffect(boolean applied) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - + void setSpanWidths(List spanWidths) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - + void setDescriptionWidth(double max) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - + @Override void setDescriptionVisibility(DescriptionVisibility get) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. @@ -134,12 +134,12 @@ public class EventClusterNode extends EventBundleNodeBase { - + private ContextMenu contextMenu; - + @Override public void handle(MouseEvent t) { - + if (t.getButton() == MouseButton.PRIMARY) { t.consume(); if (t.isShiftDown()) { @@ -152,7 +152,7 @@ public class EventClusterNode extends EventBundleNodeBase> loggedTask = new Task>() { - + private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); - + { updateTitle(Bundle.EventStripeNode_loggedTask_name()); } - + @Override protected Collection call() throws Exception { Collection bundles; @@ -235,15 +235,15 @@ public class EventClusterNode extends EventBundleNodeBase bundles = get(); - + if (bundles.isEmpty()) { } else { chart.getEventBundles().addAll(bundles); @@ -268,38 +268,38 @@ public class EventClusterNode extends EventBundleNodeBase { final DescriptionLoD next = descLOD.get().moreDetailed(); if (next != null) { loadSubBundles(DescriptionLoD.RelativeDetail.MORE); - + } }); disabledProperty().bind(descLOD.isEqualTo(DescriptionLoD.FULL)); } } - + private class CollapseClusterAction extends Action { - + @NbBundle.Messages("CollapseClusterAction.text=Collapse") CollapseClusterAction() { super(Bundle.CollapseClusterAction_text()); - + setGraphic(new ImageView(MINUS)); setEventHandler((ActionEvent t) -> { final DescriptionLoD previous = descLOD.get().lessDetailed(); @@ -310,11 +310,11 @@ public class EventClusterNode extends EventBundleNodeBase nonNull(getEventCluster()) && descLOD.get() == getEventCluster().getDescriptionLoD(), descLOD)); } } - + EventCluster getEventCluster() { return getEventBundle(); } - + @Override protected void layoutChildren(double xOffset) { super.layoutChildren(); //To change body of generated methods, choose Tools | Templates. 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 1b31e6c76d..bc17e8f64e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -467,7 +467,7 @@ public final class EventDetailChart extends XYChart impl //hash map from y value to right most occupied x value. This tells you for a given 'pixel row' what is the first avaialable slot maxY.set(0); - List stripeNodes; + List stripeNodes; if (bandByType.get()) { for (Series series : sortedSeriesList) { stripeNodes = series.getData().stream() @@ -510,6 +510,7 @@ public final class EventDetailChart extends XYChart impl ReadOnlyDoubleProperty maxVScrollProperty() { return maxY.getReadOnlyProperty(); } + Function>> clusterFlattener = new Function>>() { @Override @@ -559,11 +560,11 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - synchronized double layoutStripes(final Collection nodes, final double minY, final double xOffset) { + synchronized double layoutStripes(final Collection> nodes, final double minY, final double xOffset) { final Map maxXatY = new HashMap<>(); double localMax = minY; //for each node size it and position it in first available slot - for (EventBundleNodeBase stripeNode : nodes) { + for (EventBundleNodeBase stripeNode : nodes) { boolean quickHide = getController().getQuickHideFilters().stream() .filter(AbstractFilter::isActive) .anyMatch(filter -> filter.getDescription().equals(stripeNode.getDescription())); From acc83ce6ef726e091114c42c3391602c3afd9073 Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 6 Oct 2015 16:42:20 -0400 Subject: [PATCH 04/13] improve UX of expand collapse buttons --- .../ui/detailview/EventBundleNodeBase.java | 10 ++- .../ui/detailview/EventClusterNode.java | 75 ++++++++++------ .../ui/detailview/EventDetailChart.java | 86 ++++++++----------- .../ui/detailview/EventStripeNode.java | 36 ++------ 4 files changed, 102 insertions(+), 105 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java index bf9806e2d0..24ed868500 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -68,6 +68,7 @@ public abstract class EventBundleNodeBase sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import static java.util.Objects.nonNull; import java.util.concurrent.ExecutionException; @@ -16,10 +30,12 @@ import javafx.beans.binding.Bindings; import javafx.concurrent.Task; import javafx.event.ActionEvent; 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.SeparatorMenuItem; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -31,6 +47,8 @@ import javafx.scene.layout.BorderStrokeStyle; import javafx.scene.layout.BorderWidths; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionUtils; import org.joda.time.DateTime; @@ -64,11 +82,15 @@ final public class EventClusterNode extends EventBundleNodeBase { + setOnMouseExited((MouseEvent event) -> { showDescriptionLoDControls(false); + chart.requestChartLayout(); }); - configureLoDButton(plusButton); configureLoDButton(minusButton); - HBox buttonBar = new HBox(5, plusButton, minusButton); + + setAlignment(Pos.CENTER_LEFT); + HBox buttonBar = new HBox(5, minusButton, plusButton); buttonBar.setMaxWidth(USE_PREF_SIZE); buttonBar.setAlignment(Pos.BOTTOM_LEFT); - - getChildren().addAll(clusterRegion, subNodePane, buttonBar); + Label label = new Label(Long.toString(getEventBundle().getCount())); + label.setPadding(new Insets(0, 3, 0, 5)); + StackPane stackPane = new StackPane(clusterRegion, label, subNodePane); + stackPane.setAlignment(Pos.CENTER_LEFT); + setAlignment(stackPane, Pos.TOP_LEFT); + VBox vBox = new VBox(stackPane, buttonBar); + getChildren().addAll(vBox); } void showDescriptionLoDControls(final boolean showControls) { - show(minusButton, showControls); show(plusButton, showControls); + show(minusButton, showControls); } @Override @@ -192,7 +220,6 @@ final public class EventClusterNode extends EventBundleNodeBase impl */ private final InvalidationListener layoutInvalidationListener = (Observable o) -> { synchronized (EventDetailChart.this) { - requiresLayout = true; + requestChartLayout(); } }; @@ -136,11 +135,6 @@ public final class EventDetailChart extends XYChart impl * the maximum y value used so far during the most recent layout pass */ private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); - /** - * flag indicating whether this chart actually needs a layout pass - */ - @GuardedBy(value = "this") - private boolean requiresLayout = true; final ObservableList> selectedNodes; /** @@ -427,10 +421,6 @@ public final class EventDetailChart extends XYChart impl data.setNode(null); } - synchronized void setRequiresLayout(boolean b) { - requiresLayout = true; - } - /** * make this accessible to {@link EventStripeNode} */ @@ -462,30 +452,29 @@ public final class EventDetailChart extends XYChart impl */ @Override protected synchronized void layoutPlotChildren() { - if (requiresLayout) { - setCursor(Cursor.WAIT); - //hash map from y value to right most occupied x value. This tells you for a given 'pixel row' what is the first avaialable slot - maxY.set(0); - List stripeNodes; - if (bandByType.get()) { - for (Series series : sortedSeriesList) { - stripeNodes = series.getData().stream() - .map(data -> (EventStripeNode) data.getNode()) - .sorted(Comparator.comparing(EventStripeNode::getStartMillis)). - collect(Collectors.toList()); - maxY.set(maxY.get() + layoutStripes(stripeNodes, maxY.get(), 0)); + setCursor(Cursor.WAIT); + + //hash map from y value to right most occupied x value. This tells you for a given 'pixel row' what is the first avaialable slot + maxY.set(0); + List stripeNodes; + if (bandByType.get()) { + for (Series series : sortedSeriesList) { + stripeNodes = series.getData().stream() + .map(data -> (EventStripeNode) data.getNode()) + .sorted(Comparator.comparing(EventStripeNode::getStartMillis)). + collect(Collectors.toList()); + maxY.set(maxY.get() + layoutEventBundleNodes(stripeNodes, maxY.get(), 0)); - } - } else { - stripeNodes = stripeNodeMap.values().stream() - .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) - .collect(Collectors.toList()); - maxY.set(layoutStripes(stripeNodes, 0, 0)); } - setCursor(null); - requiresLayout = false; + } else { + stripeNodes = stripeNodeMap.values().stream() + .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) + .collect(Collectors.toList()); + maxY.set(layoutEventBundleNodes(stripeNodes, 0, 0)); } + setCursor(null); + layoutProjectionMap(); } @@ -495,7 +484,7 @@ public final class EventDetailChart extends XYChart impl dataItemAdded(series, j, series.getData().get(j)); } seriesList.add(series); - requiresLayout = true; + requestLayout(); } @Override @@ -504,7 +493,7 @@ public final class EventDetailChart extends XYChart impl dataItemRemoved(series.getData().get(j), series); } seriesList.remove(series); - requiresLayout = true; + requestLayout(); } ReadOnlyDoubleProperty maxVScrollProperty() { @@ -560,27 +549,27 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - synchronized double layoutStripes(final Collection> nodes, final double minY, final double xOffset) { + synchronized double layoutEventBundleNodes(final Collection> nodes, final double minY, final double xOffset) { final Map maxXatY = new HashMap<>(); double localMax = minY; //for each node size it and position it in first available slot - for (EventBundleNodeBase stripeNode : nodes) { + for (EventBundleNodeBase bundleNode : nodes) { boolean quickHide = getController().getQuickHideFilters().stream() .filter(AbstractFilter::isActive) - .anyMatch(filter -> filter.getDescription().equals(stripeNode.getDescription())); + .anyMatch(filter -> filter.getDescription().equals(bundleNode.getDescription())); if (quickHide) { - stripeNode.setVisible(false); - stripeNode.setManaged(false); + bundleNode.setVisible(false); + bundleNode.setManaged(false); } else { - stripeNode.setVisible(true); - stripeNode.setManaged(true); - stripeNode.setDescriptionVisibility(descrVisibility.get()); - stripeNode.layoutChildren(xOffset); - double xLeft = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis())) - xOffset; - double xRight = xLeft + stripeNode.getBoundsInParent().getWidth(); + bundleNode.setVisible(true); + bundleNode.setManaged(true); + bundleNode.setDescriptionVisibility(descrVisibility.get()); + bundleNode.layoutChildren(xOffset); + double xLeft = getXAxis().getDisplayPosition(new DateTime(bundleNode.getStartMillis())) - xOffset; + double xRight = xLeft + bundleNode.getBoundsInParent().getWidth(); //get the height of the node - final double h = stripeNode.getBoundsInParent().getHeight(); + final double h = bundleNode.getBoundsInParent().getHeight(); //initial test position double yTop = minY; @@ -615,9 +604,10 @@ public final class EventDetailChart extends XYChart impl } localMax = Math.max(yBottom, localMax); - Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), - new KeyValue(stripeNode.layoutXProperty(), xLeft), - new KeyValue(stripeNode.layoutYProperty(), yTop))); +// bundleNode.relocate(xLeft, yTop); + Timeline tm = new Timeline(new KeyFrame(Duration.seconds(.5), + new KeyValue(bundleNode.layoutXProperty(), xLeft), + new KeyValue(bundleNode.layoutYProperty(), yTop))); tm.play(); } 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 5e0bb3a13f..26be97fea0 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -50,9 +50,9 @@ import javafx.scene.layout.BorderWidths; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import static javafx.scene.layout.Region.USE_PREF_SIZE; +import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import org.apache.commons.lang3.StringUtils; -import org.joda.time.DateTime; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; @@ -108,7 +108,7 @@ final public class EventStripeNode extends EventBundleNodeBase new DropShadow(10, eventType.getColor())); subNodePane.setEffect(showControls ? dropShadow : null); -// show(minusButton, showControls); -// show(plusButton, showControls); show(hideButton, showControls); } @@ -187,13 +184,6 @@ final public class EventStripeNode extends EventBundleNodeBase makeBundlesFromClusters(List eventClusters) { -// return eventClusters.stream().collect( -// Collectors.toMap((eventCluster) -> eventCluster.getDescription(), //key -// (eventCluster) -> new EventStripe(eventCluster,this), //value -// EventStripe::merge)//merge method -// ).values(); -// } @NbBundle.Messages({"# {0} - counts", "# {1} - event type", "# {2} - description", @@ -354,17 +344,5 @@ final public class EventStripeNode extends EventBundleNodeBase Date: Tue, 6 Oct 2015 17:11:36 -0400 Subject: [PATCH 05/13] reinstate truncate descriptions feature and remove dead code --- .../timeline/ui/detailview/EventBundleNodeBase.java | 6 +++++- .../timeline/ui/detailview/EventClusterNode.java | 9 ++++----- .../timeline/ui/detailview/EventDetailChart.java | 11 +++++++---- .../timeline/ui/detailview/EventStripeNode.java | 13 +------------ 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java index 24ed868500..32aed15069 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -141,7 +141,11 @@ public abstract class EventBundleNodeBase spanWidths) { -// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } + @Override void setDescriptionWidth(double max) { // throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java index 91a47aa072..8f72507a15 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -125,10 +125,7 @@ public final class EventDetailChart extends XYChart impl * listener that triggers layout pass */ private final InvalidationListener layoutInvalidationListener = (Observable o) -> { - synchronized (EventDetailChart.this) { - - requestChartLayout(); - } + requestChartLayout(); }; /** @@ -564,7 +561,13 @@ public final class EventDetailChart extends XYChart impl bundleNode.setVisible(true); bundleNode.setManaged(true); bundleNode.setDescriptionVisibility(descrVisibility.get()); + bundleNode.layoutChildren(xOffset); + if (truncateAll.get()) { //if truncate option is selected limit width of description label + bundleNode.setDescriptionWidth(truncateWidth.get()); + } else { //else set it unbounded + bundleNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); + } double xLeft = getXAxis().getDisplayPosition(new DateTime(bundleNode.getStartMillis())) - xOffset; double xRight = xLeft + bundleNode.getBoundsInParent().getWidth(); 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 26be97fea0..ee519de1c2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -20,7 +20,6 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -48,7 +47,6 @@ import javafx.scene.layout.BorderStroke; import javafx.scene.layout.BorderStrokeStyle; import javafx.scene.layout.BorderWidths; import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; import static javafx.scene.layout.Region.USE_PREF_SIZE; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; @@ -169,16 +167,7 @@ final public class EventStripeNode extends EventBundleNodeBase spanWidths) { - for (int i = 0; i < spanWidths.size(); i++) { - Region spanRegion = (Region) subNodePane.getChildren().get(i); - - Double w = spanWidths.get(i); - spanRegion.setPrefWidth(w); - spanRegion.setMaxWidth(w); - spanRegion.setMinWidth(Math.max(2, w)); - } - } + public EventStripe getEventStripe() { return getEventBundle(); From 79f490a2cc5216725751737a06f0fda5ea275425 Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 7 Oct 2015 10:57:49 -0400 Subject: [PATCH 06/13] hide parent controls on hover --- .../ui/detailview/EventBundleNodeBase.java | 6 ++++++ .../timeline/ui/detailview/EventClusterNode.java | 13 ++++++------- .../timeline/ui/detailview/EventDetailChart.java | 8 ++++---- .../timeline/ui/detailview/EventStripeNode.java | 15 ++++++++------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java index 32aed15069..f78a0163c5 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -116,6 +116,12 @@ public abstract class EventBundleNodeBase { - showDescriptionLoDControls(false); + showHoverControls(false); chart.requestChartLayout(); }); configureLoDButton(plusButton); @@ -124,11 +123,12 @@ final public class EventClusterNode extends EventBundleNodeBase impl if (oneEventPerRow.get()) { // if onePerRow, just put it at end - yTop = (localMax + 2); + yTop = (localMax + MINIMUM_GAP); yBottom = yTop + h; } else { boolean overlapping = true; @@ -590,11 +590,11 @@ public final class EventDetailChart extends XYChart impl //check each pixel from bottom to top. for (double y = yBottom; y >= yTop; y--) { final Double maxX = maxXatY.get((int) y); - if (maxX != null && maxX >= xLeft - 4) { + if (maxX != null && maxX >= xLeft - MINIMUM_GAP) { //if that pixel is already used //jump top to this y value and repeat until free slot is found. overlapping = true; - yTop = y + 4; + yTop = y + MINIMUM_GAP; yBottom = yTop + h; break; } @@ -607,7 +607,6 @@ public final class EventDetailChart extends XYChart impl } localMax = Math.max(yBottom, localMax); -// bundleNode.relocate(xLeft, yTop); Timeline tm = new Timeline(new KeyFrame(Duration.seconds(.5), new KeyValue(bundleNode.layoutXProperty(), xLeft), new KeyValue(bundleNode.layoutYProperty(), yTop))); @@ -617,6 +616,7 @@ public final class EventDetailChart extends XYChart impl } return localMax; } + private static final int MINIMUM_GAP = 4; private void layoutProjectionMap() { for (final Map.Entry entry : projectionMap.entrySet()) { 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 ee519de1c2..0389e02460 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -106,7 +106,7 @@ final public class EventStripeNode extends EventBundleNodeBase { /* + * w * defer tooltip creation till needed, this had a surprisingly large * impact on speed of loading the chart */ installTooltip(); - showDescriptionLoDControls(true); + showHoverControls(true); // toFront(); }); setOnMouseExited((MouseEvent e) -> { - showDescriptionLoDControls(false); + showHoverControls(false); }); } - void showDescriptionLoDControls(final boolean showControls) { + @Override + void showHoverControls(final boolean showControls) { + super.showHoverControls(showControls); DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), eventType -> new DropShadow(10, eventType.getColor())); subNodePane.setEffect(showControls ? dropShadow : null); show(hideButton, showControls); - } - + } public EventStripe getEventStripe() { return getEventBundle(); @@ -333,5 +335,4 @@ final public class EventStripeNode extends EventBundleNodeBase Date: Wed, 7 Oct 2015 14:56:51 -0400 Subject: [PATCH 07/13] refactor and improve layout move more common code into EventBundleNodeBase fix band by type option --- .../ui/detailview/EventBundleNodeBase.java | 186 +++++++++++++-- .../ui/detailview/EventClusterNode.java | 223 ++++++++---------- .../ui/detailview/EventDetailChart.java | 90 ++++--- .../ui/detailview/EventStripeNode.java | 186 ++------------- 4 files changed, 327 insertions(+), 358 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java index f78a0163c5..af8a5e081a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -6,36 +6,79 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; import javafx.beans.property.SimpleObjectProperty; +import javafx.concurrent.Task; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +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 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.controlsfx.control.action.ActionUtils; import org.joda.time.DateTime; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventStripeNode.configureLoDButton; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; /** * */ public abstract class EventBundleNodeBase, ParentType extends EventBundle, ParentNodeType extends EventBundleNodeBase> extends StackPane { + private static final Logger LOGGER = Logger.getLogger(EventBundleNodeBase.class.getName()); + private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N + private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N + static final CornerRadii CORNER_RADII_3 = new CornerRadii(3); static final CornerRadii CORNER_RADII_1 = new CornerRadii(1); + + private static final Border SELECTION_BORDER = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2))); + private static final Map dropShadowMap = new ConcurrentHashMap<>(); + + static void configureLoDButton(Button b) { + b.setMinSize(16, 16); + b.setMaxSize(16, 16); + b.setPrefSize(16, 16); + show(b, false); + } + + static void show(Node b, boolean show) { + b.setVisible(show); + b.setManaged(show); + } + protected final EventDetailChart chart; final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); protected final BundleType eventBundle; @@ -48,9 +91,17 @@ public abstract class EventBundleNodeBase subNodes = new ArrayList<>(); final Pane subNodePane = new Pane(); + final Label descrLabel = new Label(); + final Label countLabel = new Label(); + + final ImageView hashIV = new ImageView(HASH_PIN); + final ImageView tagIV = new ImageView(TAG); + final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV); + + private Tooltip tooltip; public EventBundleNodeBase(EventDetailChart chart, BundleType eventBundle, ParentNodeType parentNode) { this.eventBundle = eventBundle; @@ -64,32 +115,59 @@ public abstract class EventBundleNodeBase { + /* + * defer tooltip creation till needed, this had a surprisingly large + * impact on speed of loading the chart + */ + installTooltip(); + showHoverControls(true); + toFront(); + }); + setOnMouseExited((MouseEvent event) -> { + showHoverControls(false); + if (parentNode != null) { + parentNode.showHoverControls(true); + } + }); } final DescriptionLoD getDescriptionLoD() { return descLOD.get(); } - final public BundleType getEventBundle() { + public final BundleType getEventBundle() { return eventBundle; } @@ -98,9 +176,82 @@ public abstract class EventBundleNodeBase tooltTipTask = new Task() { - abstract void applySelectionEffect(boolean selected); + @Override + protected String call() throws Exception { + HashMap hashSetCounts = new HashMap<>(); + if (eventBundle.getEventIDsWithHashHits().isEmpty() == false) { + try { + //TODO:push this to DB + for (TimeLineEvent tle : eventsModel.getEventsById(eventBundle.getEventIDsWithHashHits())) { + Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); + for (String hashSetName : hashSetNames) { + hashSetCounts.merge(hashSetName, 1L, Long::sum); + } + } + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex); + } + } + String hashSetCountsString = hashSetCounts.entrySet().stream() + .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) + .collect(Collectors.joining("\n")); + + Map tagCounts = new HashMap<>(); + if (eventBundle.getEventIDsWithTags().isEmpty() == false) { + tagCounts.putAll(eventsModel.getTagCountsByTagName(eventBundle.getEventIDsWithTags())); + } + String tagCountsString = tagCounts.entrySet().stream() + .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) + .collect(Collectors.joining("\n")); + + return Bundle.EventBundleNodeBase_tooltip_text(getEventIDs().size(), getEventType(), getDescription(), + TimeLineController.getZonedFormatter().print(getStartMillis()), + TimeLineController.getZonedFormatter().print(getEndMillis() + 1000)) + + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) + + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString); + } + + @Override + protected void succeeded() { + super.succeeded(); + try { + if (tooltip != null) { + tooltip.hide(); + } + Tooltip.uninstall(EventBundleNodeBase.this, tooltip); + tooltip = new Tooltip(get()); + tooltip.setAutoHide(true); + Tooltip.install(EventBundleNodeBase.this, tooltip); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex); + Tooltip.uninstall(EventBundleNodeBase.this, tooltip); + tooltip = null; + } + } + }; + + chart.getController().monitorTask(tooltTipTask); + } + } + + /** + * apply the 'effect' to visually indicate selection + * + * @param applied true to apply the selection 'effect', false to remove it + */ + public void applySelectionEffect(boolean applied) { + setBorder(applied ? SELECTION_BORDER : null); + } /** * apply the 'effect' to visually indicate highlighted nodes @@ -117,7 +268,11 @@ public abstract class EventBundleNodeBase new DropShadow(10, eventType.getColor())); +// subNodePane.setEffect(showControls ? dropShadow : null); + setEffect(showControls ? dropShadow : null); + if (parentNode != null) { parentNode.showHoverControls(false); } } @@ -138,20 +293,21 @@ public abstract class EventBundleNodeBase getEventsIDs() { + final Set getEventIDs() { return getEventBundle().getEventIDs(); } double layoutChildren(double xOffset) { - super.layoutChildren(); double chartX = chart.getXAxis().getDisplayPosition(new DateTime(getStartMillis())); //position of start and end according to range of axis chart.layoutEventBundleNodes(subNodes, 0, chartX); - return chartX; + super.layoutChildren(); + return getHeight(); } /** * @param w the maximum width the description label should have */ abstract void setDescriptionWidth(double w); + } 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 08c2e5f679..8da1ca83aa 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -29,12 +29,10 @@ import javafx.beans.binding.Bindings; import javafx.concurrent.Task; import javafx.event.ActionEvent; 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.SeparatorMenuItem; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -44,10 +42,7 @@ import javafx.scene.layout.Border; import javafx.scene.layout.BorderStroke; import javafx.scene.layout.BorderStrokeStyle; import javafx.scene.layout.BorderWidths; -import javafx.scene.layout.HBox; import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionUtils; import org.joda.time.DateTime; @@ -59,8 +54,8 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventStripeNode.configureLoDButton; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventStripeNode.show; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; @@ -72,6 +67,8 @@ final public class EventClusterNode extends EventBundleNodeBase { - /* - * defer tooltip creation till needed, this had a surprisingly large - * impact on speed of loading the chart - */ - installTooltip(); - showHoverControls(true); - chart.requestChartLayout(); - }); - setOnMouseExited((MouseEvent event) -> { - showHoverControls(false); - chart.requestChartLayout(); - }); configureLoDButton(plusButton); configureLoDButton(minusButton); setAlignment(Pos.CENTER_LEFT); - HBox buttonBar = new HBox(5, minusButton, plusButton); - buttonBar.setMaxWidth(USE_PREF_SIZE); - buttonBar.setAlignment(Pos.BOTTOM_LEFT); - Label label = new Label(Long.toString(getEventBundle().getCount())); - label.setPadding(new Insets(0, 3, 0, 5)); - StackPane stackPane = new StackPane(clusterRegion, label, subNodePane); - stackPane.setAlignment(Pos.CENTER_LEFT); - setAlignment(stackPane, Pos.TOP_LEFT); - VBox vBox = new VBox(stackPane, buttonBar); - getChildren().addAll(vBox); + infoHBox.getChildren().addAll(minusButton, plusButton); + getChildren().addAll(clusterRegion, subNodePane, infoHBox); } @Override @@ -128,17 +101,6 @@ final public class EventClusterNode extends EventBundleNodeBase { - - private ContextMenu contextMenu; - - @Override - public void handle(MouseEvent t) { - - if (t.getButton() == MouseButton.PRIMARY) { - t.consume(); - if (t.isShiftDown()) { - if (chart.selectedNodes.contains(EventClusterNode.this) == false) { - chart.selectedNodes.add(EventClusterNode.this); - } - } else if (t.isShortcutDown()) { - chart.selectedNodes.removeAll(EventClusterNode.this); - } else if (t.getClickCount() > 1) { - final DescriptionLoD next = descLOD.get().moreDetailed(); - if (next != null) { - loadSubBundles(DescriptionLoD.RelativeDetail.MORE); - - } - } else { - chart.selectedNodes.setAll(EventClusterNode.this); - } - t.consume(); - } else if (t.getButton() == MouseButton.SECONDARY) { - ContextMenu chartContextMenu = chart.getChartContextMenu(t); - if (contextMenu == null) { - contextMenu = new ContextMenu(); - contextMenu.setAutoHide(true); - - contextMenu.getItems().add(ActionUtils.createMenuItem(new ExpandClusterAction())); - contextMenu.getItems().add(ActionUtils.createMenuItem(new CollapseClusterAction())); - - contextMenu.getItems().add(new SeparatorMenuItem()); - contextMenu.getItems().addAll(chartContextMenu.getItems()); - } - contextMenu.show(EventClusterNode.this, t.getScreenX(), t.getScreenY()); - t.consume(); - } + switch (descrVis) { + case HIDDEN: + countLabel.setText(""); + descrLabel.setText(""); + break; + case COUNT_ONLY: + descrLabel.setText(""); + countLabel.setText(String.valueOf(size)); + break; + default: + case SHOWN: +// String description = getEventStripe().getDescription(); +// description = parentNode != null +// ? " ..." + StringUtils.substringAfter(description, parentNode.getDescription()) +// : description; +// descrLabel.setText(description); + countLabel.setText(String.valueOf(size)); + break; } } @@ -217,6 +153,7 @@ final public class EventClusterNode extends EventBundleNodeBase bundles = get(); if (bundles.isEmpty()) { + countLabel.setVisible(true); } else { + countLabel.setVisible(false); chart.getEventBundles().addAll(bundles); subNodes.addAll(bundles.stream() .map(EventClusterNode.this::createStripeNode) @@ -284,12 +223,12 @@ final public class EventClusterNode extends EventBundleNodeBase { + + private ContextMenu contextMenu; + + @Override + public void handle(MouseEvent t) { + + if (t.getButton() == MouseButton.PRIMARY) { + t.consume(); + if (t.isShiftDown()) { + if (chart.selectedNodes.contains(EventClusterNode.this) == false) { + chart.selectedNodes.add(EventClusterNode.this); + } + } else if (t.isShortcutDown()) { + chart.selectedNodes.removeAll(EventClusterNode.this); + } else if (t.getClickCount() > 1) { + final DescriptionLoD next = descLOD.get().moreDetailed(); + if (next != null) { + loadSubBundles(DescriptionLoD.RelativeDetail.MORE); + } + } else { + chart.selectedNodes.setAll(EventClusterNode.this); + } + t.consume(); + } else if (t.getButton() == MouseButton.SECONDARY) { + ContextMenu chartContextMenu = chart.getChartContextMenu(t); + if (contextMenu == null) { + contextMenu = new ContextMenu(); + contextMenu.setAutoHide(true); + + contextMenu.getItems().add(ActionUtils.createMenuItem(new ExpandClusterAction())); + contextMenu.getItems().add(ActionUtils.createMenuItem(new CollapseClusterAction())); + + contextMenu.getItems().add(new SeparatorMenuItem()); + contextMenu.getItems().addAll(chartContextMenu.getItems()); + } + contextMenu.show(EventClusterNode.this, t.getScreenX(), t.getScreenY()); + t.consume(); + } + } + } private class ExpandClusterAction extends Action { - @NbBundle.Messages("ExpandClusterAction.text=Expand") + @NbBundle.Messages(value = "ExpandClusterAction.text=Expand") ExpandClusterAction() { super(Bundle.ExpandClusterAction_text()); @@ -321,7 +327,7 @@ final public class EventClusterNode extends EventBundleNodeBase nonNull(getEventCluster()) && descLOD.get() == getEventCluster().getDescriptionLoD(), descLOD)); } } - - EventCluster getEventCluster() { - return getEventBundle(); - } - - @Override - double layoutChildren(double xOffset) { - double chartX = super.layoutChildren(xOffset); //To change body of generated methods, choose Tools | Templates. - double w = chart.getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX; - clusterRegion.setPrefWidth(w); - return chartX; - } - - /** - * make a new filter intersecting the global filter with description and - * type filters to restrict sub-clusters - * - */ - RootFilter getSubClusterFilter() { - RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf(); - subClusterFilter.getSubFilters().addAll( - new DescriptionFilter(getDescriptionLoD(), getDescription(), DescriptionFilter.FilterMode.INCLUDE), - new TypeFilter(getEventType())); - return subClusterFilter; - } } 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 f1afd105a6..f748dfa69f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -101,8 +101,7 @@ public final class EventDetailChart extends XYChart impl private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); private static final int PROJECTED_LINE_Y_OFFSET = 5; private static final int PROJECTED_LINE_STROKE_WIDTH = 5; - private static final int DEFAULT_ROW_HEIGHT = 24; - + private static final int MINIMUM_GAP = 4; private ContextMenu chartContextMenu; private TimeLineController controller; @@ -269,7 +268,7 @@ public final class EventDetailChart extends XYChart impl } this.controller.selectEventIDs(selectedNodes.stream() - .flatMap(detailNode -> detailNode.getEventsIDs().stream()) + .flatMap(detailNode -> detailNode.getEventIDs().stream()) .collect(Collectors.toList())); }); @@ -426,53 +425,28 @@ public final class EventDetailChart extends XYChart impl super.requestChartLayout(); } - @Override - protected void layoutChildren() { - super.layoutChildren(); - - } - - /** - * Layout the nodes representing events via the following algorithm. - * - * we start with a list of nodes (each representing an event) - sort the - * list of nodes by span start time of the underlying event - initialize - * empty map (maxXatY) from y-position to max used x-value - for each node: - * -- autosize the node (based on text label) -- get the event's start and - * end positions from the dateaxis -- size the capsule representing event - * duration -- starting from the top of the chart: --- (1)check if maxXatY - * is to the left of the start position: -------if maxXatY less than start - * position , good, put the current node here, mark end position as maxXatY, - * go to next node -------if maxXatY greater than start position, increment - * y position, do -------------check(1) again until maxXatY less than start - * position - */ @Override protected synchronized void layoutPlotChildren() { - setCursor(Cursor.WAIT); - - //hash map from y value to right most occupied x value. This tells you for a given 'pixel row' what is the first avaialable slot maxY.set(0); - List stripeNodes; if (bandByType.get()) { - for (Series series : sortedSeriesList) { - stripeNodes = series.getData().stream() - .map(data -> (EventStripeNode) data.getNode()) - .sorted(Comparator.comparing(EventStripeNode::getStartMillis)). - collect(Collectors.toList()); - maxY.set(maxY.get() + layoutEventBundleNodes(stripeNodes, maxY.get(), 0)); + stripeNodeMap.values().stream() + .collect(Collectors.groupingBy(EventStripeNode::getEventType)).values() + .forEach(inputNodes -> { + List stripeNodes = inputNodes.stream() + .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) + .collect(Collectors.toList()); - } + maxY.set(maxY.get() + layoutEventBundleNodes(stripeNodes, maxY.get(), 0)); + }); } else { - stripeNodes = stripeNodeMap.values().stream() + List stripeNodes = stripeNodeMap.values().stream() .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) .collect(Collectors.toList()); maxY.set(layoutEventBundleNodes(stripeNodes, 0, 0)); } - setCursor(null); - layoutProjectionMap(); + setCursor(null); } @Override @@ -543,10 +517,26 @@ public final class EventDetailChart extends XYChart impl * layout the nodes in the given list, starting form the given minimum y * coordinate. * + * + * Layout the nodes representing events via the following algorithm. + * + * we start with a list of nodes (each representing an event) - sort the + * list of nodes by span start time of the underlying event - initialize + * empty map (maxXatY) from y-position to max used x-value - for each node: + * -- autosize the node (based on text label) -- get the event's start and + * end positions from the dateaxis -- size the capsule representing event + * duration -- starting from the top of the chart: --- (1)check if maxXatY + * is to the left of the start position: -------if maxXatY less than start + * position , good, put the current node here, mark end position as maxXatY, + * go to next node -------if maxXatY greater than start position, increment + * y position, do -------------check(1) again until maxXatY less than start + * position + * * @param nodes * @param minY */ synchronized double layoutEventBundleNodes(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 'pixel row' what is the first avaialable slot final Map maxXatY = new HashMap<>(); double localMax = minY; //for each node size it and position it in first available slot @@ -560,20 +550,19 @@ public final class EventDetailChart extends XYChart impl } else { bundleNode.setVisible(true); bundleNode.setManaged(true); + bundleNode.setDescriptionVisibility(descrVisibility.get()); - bundleNode.layoutChildren(xOffset); if (truncateAll.get()) { //if truncate option is selected limit width of description label bundleNode.setDescriptionWidth(truncateWidth.get()); } else { //else set it unbounded bundleNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); } + double h = bundleNode.layoutChildren(xOffset); + System.out.println(h); double xLeft = getXAxis().getDisplayPosition(new DateTime(bundleNode.getStartMillis())) - xOffset; double xRight = xLeft + bundleNode.getBoundsInParent().getWidth(); - - //get the height of the node - final double h = bundleNode.getBoundsInParent().getHeight(); - +// final double h = bundleNode.getBoundsInParent().getHeight(); //initial test position double yTop = minY; double yBottom = yTop + h; @@ -607,16 +596,17 @@ public final class EventDetailChart extends XYChart impl } localMax = Math.max(yBottom, localMax); - Timeline tm = new Timeline(new KeyFrame(Duration.seconds(.5), - new KeyValue(bundleNode.layoutXProperty(), xLeft), - new KeyValue(bundleNode.layoutYProperty(), yTop))); - - tm.play(); + new Timeline( + new KeyFrame(Duration.seconds(.5), + new KeyValue(bundleNode.layoutXProperty(), xLeft), + new KeyValue(bundleNode.layoutYProperty(), yTop) + ) + ).play(); } } - return localMax; + return localMax - minY; } - private static final int MINIMUM_GAP = 4; + private void layoutProjectionMap() { for (final Map.Entry entry : projectionMap.entrySet()) { 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 0389e02460..23f236b3a5 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -19,75 +19,28 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; -import java.util.stream.Collectors; -import javafx.concurrent.Task; import javafx.event.EventHandler; -import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; -import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; import javafx.scene.control.OverrunStyle; -import javafx.scene.control.SeparatorMenuItem; -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.Border; -import javafx.scene.layout.BorderStroke; -import javafx.scene.layout.BorderStrokeStyle; -import javafx.scene.layout.BorderWidths; -import javafx.scene.layout.HBox; -import static javafx.scene.layout.Region.USE_PREF_SIZE; import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; import org.apache.commons.lang3.StringUtils; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.controlsfx.control.action.ActionUtils; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; -import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.datamodel.TskCoreException; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton; /** * Node used in {@link EventDetailChart} to represent an EventStripe. */ final public class EventStripeNode extends EventBundleNodeBase { - private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName()); - private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N - - private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N - - private final static Map dropShadowMap = new ConcurrentHashMap<>(); - private static final Border SELECTION_BORDER = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2))); - - static void configureLoDButton(Button b) { - b.setMinSize(16, 16); - b.setMaxSize(16, 16); - b.setPrefSize(16, 16); - show(b, false); - } - - static void show(Node b, boolean show) { - b.setVisible(show); - b.setManaged(show); - } - - private DescriptionVisibility descrVis; - private Tooltip tooltip; - + final Button hideButton; /** * Pane that contains EventStripeNodes for any 'subevents' if they are * displayed @@ -97,39 +50,22 @@ final public class EventStripeNode extends EventBundleNodeBase { - /* - * w - * defer tooltip creation till needed, this had a surprisingly large - * impact on speed of loading the chart - */ - installTooltip(); - showHoverControls(true); -// toFront(); - }); - - setOnMouseExited((MouseEvent e) -> { - showHoverControls(false); - }); - } @Override void showHoverControls(final boolean showControls) { super.showHoverControls(showControls); - DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), - eventType -> new DropShadow(10, eventType.getColor())); - subNodePane.setEffect(showControls ? dropShadow : null); show(hideButton, showControls); - } public EventStripe getEventStripe() { return getEventBundle(); } - @NbBundle.Messages({"# {0} - counts", - "# {1} - event type", - "# {2} - description", - "# {3} - start date/time", - "# {4} - end date/time", - "EventStripeNode.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}"}) - @Override - synchronized void installTooltip() { - if (tooltip == null) { - final Task tooltTipTask = new Task() { - - @Override - protected String call() throws Exception { - HashMap hashSetCounts = new HashMap<>(); - if (!getEventStripe().getEventIDsWithHashHits().isEmpty()) { - hashSetCounts = new HashMap<>(); - try { - for (TimeLineEvent tle : eventsModel.getEventsById(getEventStripe().getEventIDsWithHashHits())) { - Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); - for (String hashSetName : hashSetNames) { - hashSetCounts.merge(hashSetName, 1L, Long::sum); - } - } - } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex); - } - } - - Map tagCounts = new HashMap<>(); - if (getEventStripe().getEventIDsWithTags().isEmpty() == false) { - tagCounts.putAll(eventsModel.getTagCountsByTagName(getEventStripe().getEventIDsWithTags())); - } - - String hashSetCountsString = hashSetCounts.entrySet().stream() - .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) - .collect(Collectors.joining("\n")); - String tagCountsString = tagCounts.entrySet().stream() - .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) - .collect(Collectors.joining("\n")); - return Bundle.EventStripeNode_tooltip_text(getEventStripe().getEventIDs().size(), getEventStripe().getEventType(), getEventStripe().getDescription(), - TimeLineController.getZonedFormatter().print(getEventStripe().getStartMillis()), - TimeLineController.getZonedFormatter().print(getEventStripe().getEndMillis() + 1000)) - + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) - + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString); - } - - @Override - protected void succeeded() { - super.succeeded(); - try { - tooltip = new Tooltip(get()); - Tooltip.install(EventStripeNode.this, tooltip); - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex); - Tooltip.uninstall(EventStripeNode.this, tooltip); - tooltip = null; - } - } - }; - - chart.getController().monitorTask(tooltTipTask); - } - } - /** * @param w the maximum width the description label should have */ + @Override public void setDescriptionWidth(double w) { descrLabel.setMaxWidth(w); } - /** - * 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) { - setBorder(applied ? SELECTION_BORDER : null); - } - /** * apply the 'effect' to visually indicate highlighted nodes * @@ -274,10 +115,9 @@ final public class EventStripeNode extends EventBundleNodeBase Date: Thu, 8 Oct 2015 16:55:41 -0400 Subject: [PATCH 08/13] adjust layout code for smoother layout --- .../ui/detailview/EventBundleNodeBase.java | 13 ++++----- .../ui/detailview/EventClusterNode.java | 7 +++-- .../ui/detailview/EventDetailChart.java | 29 +++++++++---------- .../ui/detailview/EventStripeNode.java | 5 +++- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java index af8a5e081a..72d80b5ae6 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -42,6 +42,7 @@ import javafx.scene.paint.Color; import org.joda.time.DateTime; import org.openide.util.NbBundle; 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.FilteredEventsModel; @@ -182,7 +183,8 @@ public abstract class EventBundleNodeBase tooltTipTask = new Task() { @@ -225,10 +227,6 @@ public abstract class EventBundleNodeBase new DropShadow(10, eventType.getColor())); -// subNodePane.setEffect(showControls ? dropShadow : null); setEffect(showControls ? dropShadow : null); if (parentNode != null) { parentNode.showHoverControls(false); @@ -297,12 +294,12 @@ public abstract class EventBundleNodeBase impl private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); private static final int PROJECTED_LINE_Y_OFFSET = 5; private static final int PROJECTED_LINE_STROKE_WIDTH = 5; - private static final int MINIMUM_GAP = 4; + private static final int MINIMUM_GAP = 4; private ContextMenu chartContextMenu; private TimeLineController controller; @@ -271,8 +271,6 @@ public final class EventDetailChart extends XYChart impl .flatMap(detailNode -> detailNode.getEventIDs().stream()) .collect(Collectors.toList())); }); - - requestChartLayout(); } ObservableList> getEventBundles() { @@ -399,6 +397,7 @@ public final class EventDetailChart extends XYChart impl stripeNodeMap.put(eventStripe, stripeNode); nodeGroup.getChildren().add(stripeNode); data.setNode(stripeNode); + layoutPlotChildren(); } @Override @@ -540,7 +539,7 @@ public final class EventDetailChart extends XYChart impl final Map maxXatY = new HashMap<>(); double localMax = minY; //for each node size it and position it in first available slot - for (EventBundleNodeBase bundleNode : nodes) { + for (final EventBundleNodeBase bundleNode : nodes) { boolean quickHide = getController().getQuickHideFilters().stream() .filter(AbstractFilter::isActive) .anyMatch(filter -> filter.getDescription().equals(bundleNode.getDescription())); @@ -550,19 +549,16 @@ public final class EventDetailChart extends XYChart impl } else { bundleNode.setVisible(true); bundleNode.setManaged(true); - bundleNode.setDescriptionVisibility(descrVisibility.get()); + bundleNode.setDescriptionWidth(truncateAll.get() + ? truncateWidth.get() + : USE_PREF_SIZE); + bundleNode.layout(); + double h = bundleNode.getBoundsInLocal().getHeight(); + System.out.println(h + " " + bundleNode.getClass().getName()); - if (truncateAll.get()) { //if truncate option is selected limit width of description label - bundleNode.setDescriptionWidth(truncateWidth.get()); - } else { //else set it unbounded - bundleNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); - } - double h = bundleNode.layoutChildren(xOffset); - System.out.println(h); double xLeft = getXAxis().getDisplayPosition(new DateTime(bundleNode.getStartMillis())) - xOffset; - double xRight = xLeft + bundleNode.getBoundsInParent().getWidth(); -// final double h = bundleNode.getBoundsInParent().getHeight(); + double xRight = xLeft + bundleNode.getBoundsInLocal().getWidth(); //initial test position double yTop = minY; double yBottom = yTop + h; @@ -596,17 +592,18 @@ public final class EventDetailChart extends XYChart impl } localMax = Math.max(yBottom, localMax); +// bundleNode.relocate(xLeft, yTop); new Timeline( - new KeyFrame(Duration.seconds(.5), + new KeyFrame(Duration.seconds(1), new KeyValue(bundleNode.layoutXProperty(), xLeft), new KeyValue(bundleNode.layoutYProperty(), yTop) ) ).play(); + } } return localMax - minY; } - private void layoutProjectionMap() { for (final Map.Entry entry : projectionMap.entrySet()) { 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 23f236b3a5..c5743c2ce3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -31,6 +31,7 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.VBox; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.action.ActionUtils; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton; @@ -40,6 +41,7 @@ import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.c */ final public class EventStripeNode extends EventBundleNodeBase { + private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName()); final Button hideButton; /** * Pane that contains EventStripeNodes for any 'subevents' if they are @@ -54,7 +56,8 @@ final public class EventStripeNode extends EventBundleNodeBase Date: Fri, 9 Oct 2015 12:21:25 -0400 Subject: [PATCH 09/13] improve layout so it does not jitter (as much) when clusters are selected --- .../ui/detailview/EventBundleNodeBase.java | 27 ++++--- .../ui/detailview/EventClusterNode.java | 31 ++++---- .../ui/detailview/EventDetailChart.java | 73 ++++++++++++------- .../ui/detailview/EventStripeNode.java | 4 +- 4 files changed, 75 insertions(+), 60 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java index 72d80b5ae6..7d0d985405 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -14,6 +14,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.stream.Collectors; +import javafx.beans.Observable; import javafx.beans.property.SimpleObjectProperty; import javafx.concurrent.Task; import javafx.geometry.Insets; @@ -23,6 +24,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.effect.DropShadow; +import javafx.scene.effect.Effect; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; @@ -65,8 +67,8 @@ public abstract class EventBundleNodeBase dropShadowMap = new ConcurrentHashMap<>(); + private final Border SELECTION_BORDER; + private static final Map dropShadowMap = new ConcurrentHashMap<>(); static void configureLoDButton(Button b) { b.setMinSize(16, 16); @@ -115,7 +117,7 @@ public abstract class EventBundleNodeBase { + chart.layoutPlotChildren(); + }); setMaxHeight(USE_PREF_SIZE); setMaxWidth(USE_PREF_SIZE); setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()); @@ -140,7 +145,6 @@ public abstract class EventBundleNodeBase new DropShadow(10, eventType.getColor())); + Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(), + eventType -> new DropShadow(-10, eventType.getColor())); setEffect(showControls ? dropShadow : null); if (parentNode != null) { parentNode.showHoverControls(false); @@ -296,9 +301,7 @@ public abstract class EventBundleNodeBase impl * listener that triggers layout pass */ private final InvalidationListener layoutInvalidationListener = (Observable o) -> { - requestChartLayout(); + layoutPlotChildren(); }; /** @@ -206,8 +208,6 @@ public final class EventDetailChart extends XYChart impl getPlotChildren().add(nodeGroup); //add listener for events that should trigger layout - widthProperty().addListener(layoutInvalidationListener); - heightProperty().addListener(layoutInvalidationListener); bandByType.addListener(layoutInvalidationListener); oneEventPerRow.addListener(layoutInvalidationListener); truncateAll.addListener(layoutInvalidationListener); @@ -414,14 +414,7 @@ public final class EventDetailChart extends XYChart impl EventStripeNode removedNode = stripeNodeMap.remove(removedStripe); nodeGroup.getChildren().remove(removedNode); data.setNode(null); - } - - /** - * make this accessible to {@link EventStripeNode} - */ - @Override - protected void requestChartLayout() { - super.requestChartLayout(); + layoutPlotChildren(); } @Override @@ -436,14 +429,18 @@ public final class EventDetailChart extends XYChart impl .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) .collect(Collectors.toList()); - maxY.set(maxY.get() + layoutEventBundleNodes(stripeNodes, maxY.get(), 0)); + maxY.set(maxY.get() + layoutEventBundleNodes(stripeNodes, maxY.get())); }); } else { List stripeNodes = stripeNodeMap.values().stream() .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) .collect(Collectors.toList()); - maxY.set(layoutEventBundleNodes(stripeNodes, 0, 0)); + maxY.set(layoutEventBundleNodes(stripeNodes, 0)); } + + Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(.5), keys.toArray(new KeyValue[keys.size()]))); + timeline.play(); + keys.clear(); layoutProjectionMap(); setCursor(null); } @@ -454,7 +451,7 @@ public final class EventDetailChart extends XYChart impl dataItemAdded(series, j, series.getData().get(j)); } seriesList.add(series); - requestLayout(); + } @Override @@ -463,7 +460,7 @@ public final class EventDetailChart extends XYChart impl dataItemRemoved(series.getData().get(j), series); } seriesList.remove(series); - requestLayout(); + } ReadOnlyDoubleProperty maxVScrollProperty() { @@ -534,7 +531,7 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - synchronized double layoutEventBundleNodes(final Collection> nodes, final double minY, final double xOffset) { + synchronized double layoutEventBundleNodes(final Collection> nodes, final double minY) { //hash map from y value to right most occupied x value. This tells you for a given 'pixel row' what is the first avaialable slot final Map maxXatY = new HashMap<>(); double localMax = minY; @@ -553,12 +550,14 @@ public final class EventDetailChart extends XYChart impl bundleNode.setDescriptionWidth(truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE); - bundleNode.layout(); + bundleNode.layoutChildren(); double h = bundleNode.getBoundsInLocal().getHeight(); - System.out.println(h + " " + bundleNode.getClass().getName()); + double w = bundleNode.getBoundsInLocal().getWidth(); - double xLeft = getXAxis().getDisplayPosition(new DateTime(bundleNode.getStartMillis())) - xOffset; - double xRight = xLeft + bundleNode.getBoundsInLocal().getWidth(); + double xLeft = getXAxis().getDisplayPosition(new DateTime(bundleNode.getStartMillis())) - bundleNode.getLayoutXCompensation(); + + System.out.println(h + " x " + w + " " + bundleNode.getClass().getName()); + double xRight = xLeft + w; //initial test position double yTop = minY; double yBottom = yTop + h; @@ -587,24 +586,22 @@ public final class EventDetailChart extends XYChart impl } //mark used y values for (double y = yTop; y <= yBottom; y++) { - maxXatY.put((int) y, xRight); + maxXatY.put((int) Math.floor(y), xRight); } } localMax = Math.max(yBottom, localMax); // bundleNode.relocate(xLeft, yTop); - new Timeline( - new KeyFrame(Duration.seconds(1), - new KeyValue(bundleNode.layoutXProperty(), xLeft), - new KeyValue(bundleNode.layoutYProperty(), yTop) - ) - ).play(); - + keys.add( + new KeyValue(bundleNode.layoutXProperty(), xLeft)); + keys.add(new KeyValue(bundleNode.layoutYProperty(), yTop)); } } return localMax - minY; } + HashSet keys = new HashSet<>(); + private void layoutProjectionMap() { for (final Map.Entry entry : projectionMap.entrySet()) { final EventCluster cluster = entry.getKey(); @@ -656,6 +653,26 @@ public final class EventDetailChart extends XYChart impl } } + private static class LayoutResult { + + private final double height; + private final Set keys; + + LayoutResult(double height, Set keys) { + this.height = height; + this.keys = keys; + } + + public double getHeight() { + return height; + } + + public Set getKeys() { + return Collections.unmodifiableSet(keys); + } + + } + private class PlaceMarkerAction extends Action { PlaceMarkerAction(MouseEvent clickEvent) { 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 c5743c2ce3..784191b485 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -56,7 +56,7 @@ final public class EventStripeNode extends EventBundleNodeBase Date: Tue, 13 Oct 2015 10:18:01 -0400 Subject: [PATCH 10/13] play layout animations in seperate timelines --- .../ui/detailview/EventDetailChart.java | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) 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 e683e2007d..df70cac856 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -18,12 +18,13 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; +import com.google.common.collect.Range; +import com.google.common.collect.TreeRangeMap; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.MissingResourceException; @@ -437,10 +438,6 @@ public final class EventDetailChart extends XYChart impl .collect(Collectors.toList()); maxY.set(layoutEventBundleNodes(stripeNodes, 0)); } - - Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(.5), keys.toArray(new KeyValue[keys.size()]))); - timeline.play(); - keys.clear(); layoutProjectionMap(); setCursor(null); } @@ -532,8 +529,11 @@ public final class EventDetailChart extends XYChart impl * @param minY */ synchronized double layoutEventBundleNodes(final Collection> nodes, final double minY) { - //hash map from y value to right most occupied x value. This tells you for a given 'pixel row' what is the first avaialable slot - final Map maxXatY = new HashMap<>(); + /* + * map from y value (ranges) to right most occupied x value. + */ + TreeRangeMap treeRangeMap = TreeRangeMap.create(); + double localMax = minY; //for each node size it and position it in first available slot for (final EventBundleNodeBase bundleNode : nodes) { @@ -550,13 +550,10 @@ public final class EventDetailChart extends XYChart impl bundleNode.setDescriptionWidth(truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE); - bundleNode.layoutChildren(); + bundleNode.layout(); double h = bundleNode.getBoundsInLocal().getHeight(); double w = bundleNode.getBoundsInLocal().getWidth(); - double xLeft = getXAxis().getDisplayPosition(new DateTime(bundleNode.getStartMillis())) - bundleNode.getLayoutXCompensation(); - - System.out.println(h + " x " + w + " " + bundleNode.getClass().getName()); double xRight = xLeft + w; //initial test position double yTop = minY; @@ -573,7 +570,7 @@ public final class EventDetailChart extends XYChart impl overlapping = false; //check each pixel from bottom to top. for (double y = yBottom; y >= yTop; y--) { - final Double maxX = maxXatY.get((int) y); + final Double maxX = treeRangeMap.get(y); if (maxX != null && maxX >= xLeft - MINIMUM_GAP) { //if that pixel is already used //jump top to this y value and repeat until free slot is found. @@ -584,24 +581,26 @@ public final class EventDetailChart extends XYChart impl } } } - //mark used y values - for (double y = yTop; y <= yBottom; y++) { - maxXatY.put((int) Math.floor(y), xRight); - } + treeRangeMap.put(Range.closed(yTop, yBottom), xRight); } localMax = Math.max(yBottom, localMax); -// bundleNode.relocate(xLeft, yTop); - keys.add( - new KeyValue(bundleNode.layoutXProperty(), xLeft)); - keys.add(new KeyValue(bundleNode.layoutYProperty(), yTop)); + Timeline timeline = new Timeline(new KeyFrame(Duration.millis(100), + new KeyValue(bundleNode.layoutXProperty(), xLeft), + new KeyValue(bundleNode.layoutYProperty(), yTop))); + timeline.setOnFinished(new EventHandler() { + @Override + public void handle(ActionEvent event) { + requestChartLayout(); + } + }); + timeline.play(); + } } return localMax - minY; } - HashSet keys = new HashSet<>(); - private void layoutProjectionMap() { for (final Map.Entry entry : projectionMap.entrySet()) { final EventCluster cluster = entry.getKey(); From cc926e4642f7961ea7d4dcee7466b56e15ab099e Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 13 Oct 2015 13:05:57 -0400 Subject: [PATCH 11/13] cleanup, comments, minor refactoring --- .../timeline/datamodel/EventBundle.java | 2 +- .../timeline/datamodel/EventStripe.java | 2 +- .../ui/detailview/DetailViewPane.java | 3 + .../ui/detailview/EventBundleNodeBase.java | 19 +- .../ui/detailview/EventDetailChart.java | 241 ++++++++---------- 5 files changed, 128 insertions(+), 139 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java index dd088b8420..ba3333e7f7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java @@ -25,7 +25,7 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** - * + * A interface for groups of events that share some attributes in common. */ public interface EventBundle> { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index 6c4a6fa1f7..cd457d336e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -33,7 +33,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * A 'collection' of {@link EventCluster}s, all having the same type, - * description, and zoom levels. + * description, and zoom levels, but not necessarily close together in time. */ @Immutable public final class EventStripe implements EventBundle { 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 b9ce5b8a7f..40ea735938 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -91,6 +91,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; */ public class DetailViewPane extends AbstractVisualization, EventDetailChart> { + private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName()); private MultipleSelectionModel>> treeSelectionModel; @@ -475,4 +476,6 @@ public class DetailViewPane extends AbstractVisualization sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.sleuthkit.autopsy.timeline.ui.detailview; 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 df70cac856..d941e1e5e7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -28,7 +28,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.MissingResourceException; -import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -38,7 +37,6 @@ import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.InvalidationListener; import javafx.beans.Observable; -import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.SimpleBooleanProperty; @@ -48,7 +46,6 @@ import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.ActionEvent; -import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Cursor; import javafx.scene.Group; @@ -99,12 +96,12 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; */ public final class EventDetailChart extends XYChart implements TimeLineChart { - static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS - static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS + private static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS + private static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); private static final int PROJECTED_LINE_Y_OFFSET = 5; private static final int PROJECTED_LINE_STROKE_WIDTH = 5; - private static final int MINIMUM_GAP = 4; + private static final int MINIMUM_EVENT_NODE_GAP = 4; private ContextMenu chartContextMenu; private TimeLineController controller; @@ -112,7 +109,7 @@ public final class EventDetailChart extends XYChart impl private FilteredEventsModel filteredEvents; /** - * a user position-able vertical line to help the compare events + * a user positionable vertical line to help compare events */ private Line guideLine; @@ -124,7 +121,7 @@ public final class EventDetailChart extends XYChart impl private IntervalSelector intervalSelector; /** - * listener that triggers layout pass + * listener that triggers chart layout pass */ private final InvalidationListener layoutInvalidationListener = (Observable o) -> { layoutPlotChildren(); @@ -154,11 +151,6 @@ public final class EventDetailChart extends XYChart impl private final ObservableList> seriesList = FXCollections.>observableArrayList(); - private final ObservableList> sortedSeriesList = seriesList - .sorted((Series s1, Series s2) -> { - final List eventTypeNames = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList()); - return Integer.compare(eventTypeNames.indexOf(s1.getName()), eventTypeNames.indexOf(s2.getName())); - }); /** * true == layout each event type in its own band, false == mix all the * events together during layout @@ -190,18 +182,16 @@ public final class EventDetailChart extends XYChart impl * via slider if truncateAll is true */ final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); - private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true); EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { super(dateAxis, verticalAxis); dateAxis.setAutoRanging(false); verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm - verticalAxis.setTickLabelsVisible(false); verticalAxis.setTickMarkVisible(false); - setLegendVisible(false); + setPadding(Insets.EMPTY); setAlternativeColumnFillVisible(true); @@ -221,8 +211,8 @@ public final class EventDetailChart extends XYChart impl setPrefHeight(boundsInLocalProperty().get().getHeight()); }); - //set up mouse listeners - final EventHandler clickHandler = (MouseEvent clickEvent) -> { + ///////set up mouse listeners + setOnMouseClicked((MouseEvent clickEvent) -> { if (chartContextMenu != null) { chartContextMenu.hide(); } @@ -231,10 +221,7 @@ public final class EventDetailChart extends XYChart impl chartContextMenu.show(EventDetailChart.this, clickEvent.getScreenX(), clickEvent.getScreenY()); clickEvent.consume(); } - }; - - setOnMouseClicked(clickHandler); - + }); //use one handler with an if chain because it maintains state final ChartDragHandler dragHandler = new ChartDragHandler<>(this, getXAxis()); setOnMousePressed(dragHandler); @@ -242,36 +229,7 @@ public final class EventDetailChart extends XYChart impl setOnMouseDragged(dragHandler); this.selectedNodes = selectedNodes; - this.selectedNodes.addListener(( - ListChangeListener.Change> change) -> { - while (change.next()) { - change.getRemoved().forEach((EventBundleNodeBase removedNode) -> { - removedNode.getEventBundle().getClusters().forEach(cluster -> { - Line removedLine = projectionMap.remove(cluster); - getChartChildren().removeAll(removedLine); - }); - - }); - change.getAddedSubList().forEach((EventBundleNodeBase addedNode) -> { - - for (EventCluster range : addedNode.getEventBundle().getClusters()) { - - Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, - dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET - ); - line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5)); - line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); - line.setStrokeLineCap(StrokeLineCap.ROUND); - projectionMap.put(range, line); - getChartChildren().add(line); - } - }); - } - - this.controller.selectEventIDs(selectedNodes.stream() - .flatMap(detailNode -> detailNode.getEventIDs().stream()) - .collect(Collectors.toList())); - }); + this.selectedNodes.addListener(new SelectionChangeHandler()); } ObservableList> getEventBundles() { @@ -430,7 +388,7 @@ public final class EventDetailChart extends XYChart impl .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) .collect(Collectors.toList()); - maxY.set(maxY.get() + layoutEventBundleNodes(stripeNodes, maxY.get())); + maxY.set(layoutEventBundleNodes(stripeNodes, maxY.get())); }); } else { List stripeNodes = stripeNodeMap.values().stream() @@ -448,7 +406,6 @@ public final class EventDetailChart extends XYChart impl dataItemAdded(series, j, series.getData().get(j)); } seriesList.add(series); - } @Override @@ -457,35 +414,26 @@ public final class EventDetailChart extends XYChart impl dataItemRemoved(series.getData().get(j), series); } seriesList.remove(series); - } ReadOnlyDoubleProperty maxVScrollProperty() { return maxY.getReadOnlyProperty(); } - Function>> clusterFlattener = - new Function>>() { - @Override - public Stream> apply(EventClusterNode node) { - return Stream.concat( - Stream.of(node), - node.getSubNodes().stream().flatMap(stripeFlattener::apply)); - } - }; - - Function>> stripeFlattener = - new Function>>() { - - @Override - public Stream> apply(EventStripeNode node) { - return Stream.concat( - Stream.of(node), - node.getSubNodes().stream().flatMap(clusterFlattener::apply)); - } - }; - + /** + * @return all the nodes that pass the given predicate + */ Iterable> getNodes(Predicate> p) { + //use this recursive function to flatten the tree of nodes into an iterable. + Function, Stream>> stripeFlattener = + new Function, Stream>>() { + @Override + public Stream> apply(EventBundleNodeBase node) { + return Stream.concat( + Stream.of(node), + node.getSubNodes().stream().flatMap(this::apply)); + } + }; return stripeNodeMap.values().stream() .flatMap(stripeFlattener) @@ -510,72 +458,79 @@ public final class EventDetailChart extends XYChart impl * layout the nodes in the given list, starting form the given minimum y * coordinate. * - * * Layout the nodes representing events via the following algorithm. * * we start with a list of nodes (each representing an event) - sort the * list of nodes by span start time of the underlying event - initialize * empty map (maxXatY) from y-position to max used x-value - for each node: - * -- autosize the node (based on text label) -- get the event's start and - * end positions from the dateaxis -- size the capsule representing event - * duration -- starting from the top of the chart: --- (1)check if maxXatY - * is to the left of the start position: -------if maxXatY less than start - * position , good, put the current node here, mark end position as maxXatY, - * go to next node -------if maxXatY greater than start position, increment - * y position, do -------------check(1) again until maxXatY less than start - * position * - * @param nodes - * @param minY + * -- size the node based on its children (recursively) + * + * -- get the event's start position from the dateaxis + * + * -- to position node (1)check if maxXatY is to the left of the left x + * coord: if maxXatY is less than the left x coord, good, put the current + * node here, mark right x coord as maxXatY, go to next node ; if maxXatY + * greater than start position, increment y position, do check(1) again + * until maxXatY less than start position + * + * @param nodes collection of nodes to layout + * @param minY the minimum y coordinate to position the nodes at. */ synchronized double layoutEventBundleNodes(final Collection> nodes, final double minY) { - /* - * map from y value (ranges) to right most occupied x value. - */ + // map from y value (ranges) to right most occupied x value. TreeRangeMap treeRangeMap = TreeRangeMap.create(); - + // maximum y values occupied by any of the given nodes, updated as nodes are layed out. double localMax = minY; - //for each node size it and position it in first available slot + + //for each node do a recursive layout to size it and then position it in first available slot for (final EventBundleNodeBase bundleNode : nodes) { + //is the node hiden by a quick hide filter? boolean quickHide = getController().getQuickHideFilters().stream() .filter(AbstractFilter::isActive) .anyMatch(filter -> filter.getDescription().equals(bundleNode.getDescription())); if (quickHide) { + //hide it and skip layout bundleNode.setVisible(false); bundleNode.setManaged(false); } else { + //make sure it is shown bundleNode.setVisible(true); bundleNode.setManaged(true); + //apply advanced layout description visibility options bundleNode.setDescriptionVisibility(descrVisibility.get()); - bundleNode.setDescriptionWidth(truncateAll.get() - ? truncateWidth.get() - : USE_PREF_SIZE); + bundleNode.setDescriptionWidth(truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE); + + //do recursive layout bundleNode.layout(); + //get computed height and width double h = bundleNode.getBoundsInLocal().getHeight(); double w = bundleNode.getBoundsInLocal().getWidth(); - double xLeft = getXAxis().getDisplayPosition(new DateTime(bundleNode.getStartMillis())) - bundleNode.getLayoutXCompensation(); + //get left and right x coords from axis plus computed width + double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation(); double xRight = xLeft + w; + //initial test position double yTop = minY; double yBottom = yTop + h; if (oneEventPerRow.get()) { // if onePerRow, just put it at end - yTop = (localMax + MINIMUM_GAP); + yTop = (localMax + MINIMUM_EVENT_NODE_GAP); yBottom = yTop + h; } else { + //until the node is not overlapping any others try moving it down. boolean overlapping = true; while (overlapping) { - //loop through y values looking for available slot. overlapping = false; //check each pixel from bottom to top. for (double y = yBottom; y >= yTop; y--) { final Double maxX = treeRangeMap.get(y); - if (maxX != null && maxX >= xLeft - MINIMUM_GAP) { + if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) { //if that pixel is already used //jump top to this y value and repeat until free slot is found. overlapping = true; - yTop = y + MINIMUM_GAP; + yTop = y + MINIMUM_EVENT_NODE_GAP; yBottom = yTop + h; break; } @@ -583,22 +538,25 @@ public final class EventDetailChart extends XYChart impl } treeRangeMap.put(Range.closed(yTop, yBottom), xRight); } + localMax = Math.max(yBottom, localMax); + //animate node to new position Timeline timeline = new Timeline(new KeyFrame(Duration.millis(100), new KeyValue(bundleNode.layoutXProperty(), xLeft), new KeyValue(bundleNode.layoutYProperty(), yTop))); - timeline.setOnFinished(new EventHandler() { - @Override - public void handle(ActionEvent event) { - requestChartLayout(); - } + timeline.setOnFinished((ActionEvent event) -> { + requestChartLayout(); }); timeline.play(); - } } - return localMax - minY; + return localMax; //return new max + } + + private double getXForEpochMillis(Long millis) { + DateTime dateTime = new DateTime(millis, TimeLineController.getJodaTimeZone()); + return getXAxis().getDisplayPosition(new DateTime(dateTime)); } private void layoutProjectionMap() { @@ -626,10 +584,6 @@ public final class EventDetailChart extends XYChart impl return filteredEvents; } - Property alternateLayoutProperty() { - return alternateLayout; - } - static private class DetailIntervalSelector extends IntervalSelector { DetailIntervalSelector(double x, double height, Axis axis, TimeLineController controller) { @@ -652,26 +606,6 @@ public final class EventDetailChart extends XYChart impl } } - private static class LayoutResult { - - private final double height; - private final Set keys; - - LayoutResult(double height, Set keys) { - this.height = height; - this.keys = keys; - } - - public double getHeight() { - return height; - } - - public Set getKeys() { - return Collections.unmodifiableSet(keys); - } - - } - private class PlaceMarkerAction extends Action { PlaceMarkerAction(MouseEvent clickEvent) { @@ -697,6 +631,45 @@ public final class EventDetailChart extends XYChart impl } } + private class SelectionChangeHandler implements ListChangeListener> { + + private final Axis dateAxis; + + SelectionChangeHandler() { + dateAxis = getXAxis(); + } + + @Override + public void onChanged(ListChangeListener.Change> change) { + while (change.next()) { + change.getRemoved().forEach((EventBundleNodeBase removedNode) -> { + removedNode.getEventBundle().getClusters().forEach(cluster -> { + Line removedLine = projectionMap.remove(cluster); + getChartChildren().removeAll(removedLine); + }); + + }); + change.getAddedSubList().forEach((EventBundleNodeBase addedNode) -> { + + for (EventCluster range : addedNode.getEventBundle().getClusters()) { + + Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, + dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET + ); + line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5)); + line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); + line.setStrokeLineCap(StrokeLineCap.ROUND); + projectionMap.put(range, line); + getChartChildren().add(line); + } + }); + } + EventDetailChart.this.controller.selectEventIDs(selectedNodes.stream() + .flatMap(detailNode -> detailNode.getEventIDs().stream()) + .collect(Collectors.toList())); + } + } + class HideDescriptionAction extends Action { HideDescriptionAction(String description, DescriptionLoD descriptionLoD) { @@ -711,12 +684,13 @@ public final class EventDetailChart extends XYChart impl DescriptionFilter descriptionFilter = getController().getQuickHideFilters().stream() .filter(testFilter::equals) .findFirst().orElseGet(() -> { - testFilter.selectedProperty().addListener(layoutInvalidationListener); + testFilter.selectedProperty().addListener((Observable observable) -> { + layoutPlotChildren(); + }); getController().getQuickHideFilters().add(testFilter); return testFilter; }); descriptionFilter.setSelected(true); - }); } } @@ -724,7 +698,6 @@ public final class EventDetailChart extends XYChart impl class UnhideDescriptionAction extends Action { UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) { - super("Unhide"); setGraphic(new ImageView(SHOW)); setEventHandler((ActionEvent t) -> From 09270a524a407e2853cfba64c4e375dd9b6c4e53 Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 13 Oct 2015 13:48:22 -0400 Subject: [PATCH 12/13] improve layout performance by only setting the description if the visibility has actually changed --- .../timeline/ui/detailview/EventBundleNodeBase.java | 11 +++++++++++ .../timeline/ui/detailview/EventClusterNode.java | 8 +------- .../timeline/ui/detailview/EventDetailChart.java | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java index 9c742c74e4..69d757eae8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -29,6 +29,7 @@ import java.util.logging.Level; import java.util.stream.Collectors; import javafx.beans.Observable; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.concurrent.Task; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -97,6 +98,7 @@ public abstract class EventBundleNodeBase descLOD = new SimpleObjectProperty<>(); + final SimpleObjectProperty descVisibility= new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); protected final BundleType eventBundle; protected final ParentNodeType parentNode; @@ -179,6 +181,11 @@ public abstract class EventBundleNodeBase observable, DescriptionVisibility oldValue, DescriptionVisibility newValue) -> { + setDescriptionVisibility(newValue); + }); } final DescriptionLoD getDescriptionLoD() { @@ -323,4 +330,8 @@ public abstract class EventBundleNodeBase impl bundleNode.setVisible(true); bundleNode.setManaged(true); //apply advanced layout description visibility options - bundleNode.setDescriptionVisibility(descrVisibility.get()); + bundleNode.setDescriptionVisibilityLevel(descrVisibility.get()); bundleNode.setDescriptionWidth(truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE); //do recursive layout From bd9c9109e465ceb357d4f8592813e28fbfeb99eb Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 13 Oct 2015 16:41:50 -0400 Subject: [PATCH 13/13] adjust EventClusterNode subbundle loading --- .../ui/detailview/EventBundleNodeBase.java | 6 +- .../ui/detailview/EventClusterNode.java | 144 +++++++++--------- .../ui/detailview/EventStripeNode.java | 2 - .../timeline/zooming/DescriptionLoD.java | 4 +- 4 files changed, 75 insertions(+), 81 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java index 69d757eae8..ee57b6fe0d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -98,7 +98,7 @@ public abstract class EventBundleNodeBase descLOD = new SimpleObjectProperty<>(); - final SimpleObjectProperty descVisibility= new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); + final SimpleObjectProperty descVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); protected final BundleType eventBundle; protected final ParentNodeType parentNode; @@ -181,7 +181,7 @@ public abstract class EventBundleNodeBase observable, DescriptionVisibility oldValue, DescriptionVisibility newValue) -> { setDescriptionVisibility(newValue); @@ -331,7 +331,7 @@ public abstract class EventBundleNodeBase subNodes.stream().anyMatch(subNode -> bundle.equals(subNode.getEventStripe())) ); - subNodePane.getChildren().clear(); subNodes.clear(); - if (descLOD.get().withRelativeDetail(relativeDetail) == getEventBundle().getDescriptionLoD()) { - countLabel.setVisible(true); - descLOD.set(getEventBundle().getDescriptionLoD()); - chart.layoutPlotChildren(); - } else { - /* - * make new ZoomParams to query with - * - * We need to extend end time because for the query by one second, - * because it is treated as an open interval but we want to include - * events at exactly the time of the last event in this cluster - */ - final RootFilter subClusterFilter = getSubClusterFilter(); - final Interval subClusterSpan = new Interval(getStartMillis(), getEndMillis() + 1000); - final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); - final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); - Task> loggedTask = new Task>() { + /* + * make new ZoomParams to query with + * + * We need to extend end time because for the query by one second, + * because it is treated as an open interval but we want to include + * events at exactly the time of the last event in this cluster + */ + final RootFilter subClusterFilter = getSubClusterFilter(); + final Interval subClusterSpan = new Interval(getStartMillis(), getEndMillis() + 1000); + final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); + final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); - private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); + Task> loggedTask = new Task>() { - { - updateTitle(Bundle.EventStripeNode_loggedTask_name()); - } + private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); - @Override - protected Collection call() throws Exception { - Collection bundles; - DescriptionLoD next = loadedDescriptionLoD; - do { - loadedDescriptionLoD = next; - if (loadedDescriptionLoD == getEventBundle().getDescriptionLoD()) { - return Collections.emptySet(); - } - bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream() - .collect(Collectors.toMap((eventCluster) -> eventCluster.getDescription(), //key - (eventCluster) -> new EventStripe(eventCluster, getEventCluster()), //value - EventStripe::merge) //merge method - ).values(); - next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); - } while (bundles.size() == 1 && nonNull(next)); + { + updateTitle(Bundle.EventStripeNode_loggedTask_name()); + } - // return list of AbstractEventStripeNodes representing sub-bundles - return bundles; - - } - - @Override - protected void succeeded() { - chart.setCursor(Cursor.WAIT); - try { - Collection bundles = get(); - - if (bundles.isEmpty()) { - countLabel.setVisible(true); - } else { - countLabel.setVisible(false); - chart.getEventBundles().addAll(bundles); - subNodes.addAll(bundles.stream() - .map(EventClusterNode.this::createStripeNode) - .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) - .collect(Collectors.toList())); - subNodePane.getChildren().addAll(subNodes); - } - descLOD.set(loadedDescriptionLoD); - //assign subNodes and request chart layout - - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); + @Override + protected Collection call() throws Exception { + Collection bundles; + DescriptionLoD next = loadedDescriptionLoD; + do { + loadedDescriptionLoD = next; + if (loadedDescriptionLoD == getEventBundle().getDescriptionLoD()) { + return Collections.emptySet(); } - chart.layoutPlotChildren(); - chart.setCursor(null); - } - }; + bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream() + .collect(Collectors.toMap(EventCluster::getDescription, //key + (eventCluster) -> new EventStripe(eventCluster, getEventCluster()), //value + EventStripe::merge) //merge method + ).values(); + next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); + } while (bundles.size() == 1 && nonNull(next)); - //start task - chart.getController().monitorTask(loggedTask); - } + // return list of AbstractEventStripeNodes representing sub-bundles + return bundles; + + } + + @Override + protected void succeeded() { + + try { + Collection bundles = get(); + + if (bundles.isEmpty()) { + subNodePane.getChildren().clear(); + getChildren().setAll(subNodePane, infoHBox); + descLOD.set(getEventBundle().getDescriptionLoD()); + } else { + chart.getEventBundles().addAll(bundles); + subNodes.addAll(bundles.stream() + .map(EventClusterNode.this::createStripeNode) + .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) + .collect(Collectors.toList())); + subNodePane.getChildren().setAll(subNodes); + getChildren().setAll(new VBox(infoHBox, subNodePane)); + descLOD.set(loadedDescriptionLoD); + } + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); + } + chart.layoutPlotChildren(); + chart.setCursor(null); + } + }; + + //start task + chart.getController().monitorTask(loggedTask); } private EventStripeNode createStripeNode(EventStripe stripe) { @@ -249,7 +246,7 @@ final public class EventClusterNode extends EventBundleNodeBase