From 41110ae0b5bafe9d09f63f55d44d96b355fc5908 Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 28 Sep 2015 17:11:48 -0400 Subject: [PATCH 1/5] cleanup and simplify code for only new style clustering --- .../timeline/datamodel/EventStripe.java | 10 + .../ui/detailview/AbstractDetailViewNode.java | 554 ----------------- .../ui/detailview/DetailViewNode.java | 67 -- .../ui/detailview/DetailViewPane.java | 18 +- .../ui/detailview/EventClusterNode.java | 181 ------ .../ui/detailview/EventDetailChart.java | 194 +++--- .../ui/detailview/EventStripeNode.java | 584 +++++++++++++++++- .../timeline/ui/detailview/tree/NavPanel.java | 5 +- 8 files changed, 652 insertions(+), 961 deletions(-) delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index e88e8ff623..4a3f86e4f2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -15,6 +15,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; import javax.annotation.concurrent.Immutable; +import org.joda.time.DateTime; import org.python.google.common.base.Objects; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; @@ -140,4 +141,13 @@ public final class EventStripe implements EventBundle { public Iterable> getRanges() { return spans.asRanges(); } + + public DateTime getEnd() { + return spanMap.get(getStartMillis()).getSpan().getStart(); + } + + public DateTime getStart() { + return spanMap.get(getEndMillis()).getSpan().getStart(); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java deleted file mode 100644 index e52cf31217..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java +++ /dev/null @@ -1,554 +0,0 @@ -/* - * 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.ui.detailview; - -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.concurrent.ExecutionException; -import java.util.logging.Level; -import java.util.stream.Collectors; -import javafx.application.Platform; -import javafx.beans.property.SimpleObjectProperty; -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; -import javafx.scene.control.Label; -import javafx.scene.control.OverrunStyle; -import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.effect.DropShadow; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.Border; -import javafx.scene.layout.BorderStroke; -import javafx.scene.layout.BorderStrokeStyle; -import javafx.scene.layout.BorderWidths; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; -import static javafx.scene.layout.Region.USE_PREF_SIZE; -import javafx.scene.layout.StackPane; -import javafx.scene.paint.Color; -import org.apache.commons.lang3.StringUtils; -import org.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.LoggedTask; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; -import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.filters.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; - -public abstract class AbstractDetailViewNode< T extends EventBundle, S extends AbstractDetailViewNode> extends StackPane implements DetailViewNode> { - - static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); - static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS - static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS - static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS - static final CornerRadii CORNER_RADII = new CornerRadii(3); - /** - * the border to apply when this node is 'selected' - */ - static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); - private static final Logger LOGGER = Logger.getLogger(AbstractDetailViewNode.class - .getName()); - - 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 final Map dropShadowMap = new HashMap<>(); - final Color evtColor; - - private final S parentNode; - private DescriptionVisibility descrVis; - - /** - * Pane that contains AggregateEventNodes of any 'subevents' if they are - * displayed - * - * //TODO: move more of the control of subnodes/events here and out of - * EventDetail Chart - */ - private final Pane subNodePane = new Pane(); - - Pane getSubNodePane() { - return subNodePane; - } - - /** - * The ImageView used to show the icon for this node's event's type - */ - private final ImageView eventTypeImageView = new ImageView(); - - /** - * The label used to display this node's event's description - */ - final Label descrLabel = new Label(); - - /** - * The label used to display this node's event count - */ - final Label countLabel = new Label(); - - private final T eventBundle; - private final EventDetailChart chart; - private final SleuthkitCase sleuthkitCase; - - SleuthkitCase getSleuthkitCase() { - return sleuthkitCase; - } - - FilteredEventsModel getEventsModel() { - return eventsModel; - } - private final FilteredEventsModel eventsModel; - - private final Button plusButton; - private final Button minusButton; - - private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); - final HBox header; - - Region getSpacer() { - return spacer; - } - - private final Region spacer = new Region(); - - private final CollapseClusterAction collapseClusterAction; - private final ExpandClusterAction expandClusterAction; - - public AbstractDetailViewNode(EventDetailChart chart, T bundle, S parentEventNode) { - this.eventBundle = bundle; - this.parentNode = parentEventNode; - this.chart = chart; - descLOD.set(bundle.getDescriptionLOD()); - sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); - eventsModel = chart.getController().getEventsModel(); - ImageView hashIV = new ImageView(HASH_PIN); - ImageView tagIV = new ImageView(TAG); - if (eventBundle.getEventIDsWithHashHits().isEmpty()) { - show(hashIV, false); - } - if (eventBundle.getEventIDsWithTags().isEmpty()) { - show(tagIV, false); - } - - expandClusterAction = new ExpandClusterAction(); - plusButton = ActionUtils.createButton(expandClusterAction, ActionUtils.ActionTextBehavior.HIDE); - configureLODButton(plusButton); - - collapseClusterAction = new CollapseClusterAction(); - minusButton = ActionUtils.createButton(collapseClusterAction, ActionUtils.ActionTextBehavior.HIDE); - configureLODButton(minusButton); - - HBox.setHgrow(spacer, Priority.ALWAYS); - header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, minusButton, plusButton); - - header.setMinWidth(USE_PREF_SIZE); - header.setPadding(new Insets(2, 5, 2, 5)); - header.setAlignment(Pos.CENTER_LEFT); - //setup description label - evtColor = getEventType().getColor(); - - eventTypeImageView.setImage(getEventType().getFXImage()); - descrLabel.setGraphic(eventTypeImageView); - descrLabel.setPrefWidth(USE_COMPUTED_SIZE); - descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); - descrLabel.setMouseTransparent(true); - - //set up subnode pane sizing contraints - subNodePane.setPrefHeight(USE_COMPUTED_SIZE); - subNodePane.setMinHeight(USE_PREF_SIZE); - subNodePane.setMinWidth(USE_PREF_SIZE); - subNodePane.setMaxHeight(USE_PREF_SIZE); - subNodePane.setMaxWidth(USE_PREF_SIZE); - subNodePane.setPickOnBounds(false); - - setAlignment(Pos.TOP_LEFT); - setMinHeight(24); - setPrefHeight(USE_COMPUTED_SIZE); - setMaxHeight(USE_PREF_SIZE); - setOnMouseClicked(new EventMouseHandler()); - - //set up mouse hover effect and tooltip - setOnMouseEntered((MouseEvent e) -> { - //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart - installTooltip(); - showDescriptionLoDControls(true); - toFront(); - }); - - setOnMouseExited((MouseEvent e) -> { - showDescriptionLoDControls(false); - }); - setCursor(Cursor.HAND); - - setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - - setLayoutX(getChart().getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()); - } - - @Override - @SuppressWarnings("unchecked") - public List getSubNodes() { - return subNodePane.getChildrenUnmodifiable().stream() - .map(t -> (S) t) - .collect(Collectors.toList()); - } - - /** - * apply the 'effect' to visually indicate selection - * - * @param applied true to apply the selection 'effect', false to remove it - */ - @Override - public void applySelectionEffect(boolean applied) { - Platform.runLater(() -> { - if (applied) { - setBorder(selectionBorder); - } else { - setBorder(null); - } - }); - } - - /** - * - * @param showControls the value of par - */ - void showDescriptionLoDControls(final boolean showControls) { - DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), - eventType -> new DropShadow(10, eventType.getColor())); - getSpanFillNode().setEffect(showControls ? dropShadow : null); - show(minusButton, showControls); - show(plusButton, showControls); - } - - /** - * 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(getEventBundle().getDescriptionLOD(), getDescription()), - new TypeFilter(getEventType())); - return subClusterFilter; - } - - abstract Collection makeBundlesFromClusters(List eventClusters); - - abstract void showSpans(final boolean showSpans); - - /** - * @param w the maximum width the description label should have - */ - @Override - public void setDescriptionWidth(double w) { - getDescrLabel().setMaxWidth(w); - } - - abstract void installTooltip(); - - /** - * apply the 'effect' to visually indicate highlighted nodes - * - * @param applied true to apply the highlight 'effect', false to remove it - */ - @Override - public synchronized void applyHighlightEffect(boolean applied) { - if (applied) { - getDescrLabel().setStyle("-fx-font-weight: bold;"); // NON-NLS - getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY))); - setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); - } else { - getDescrLabel().setStyle("-fx-font-weight: normal;"); // NON-NLS - getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - } - } - - String getDisplayedDescription() { - return getDescrLabel().getText(); - } - - abstract Region getSpanFillNode(); - - Button getPlusButton() { - return plusButton; - } - - Button getMinusButton() { - return minusButton; - } - - public final Label getDescrLabel() { - return descrLabel; - } - - final public Label getCountLabel() { - return countLabel; - } - - public S getParentNode() { - return parentNode; - } - - @Override - public final T getEventBundle() { - return eventBundle; - } - - public final EventDetailChart getChart() { - return chart; - } - - public DescriptionLOD getDescLOD() { - return descLOD.get(); - } - - /** - * loads sub-bundles at the given Description LOD, continues - * - * @param requestedDescrLoD - * @param expand - */ - private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) { - subNodePane.getChildren().clear(); - if (descLOD.get().withRelativeDetail(relativeDetail) == getEventBundle().getDescriptionLOD()) { - descLOD.set(getEventBundle().getDescriptionLOD()); - showSpans(true); - chart.setRequiresLayout(true); - chart.requestChartLayout(); - } else { - showSpans(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(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000); - final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); - final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescLOD()); - - LoggedTask> loggedTask; - loggedTask = new LoggedTask>( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { - private Collection bundles; - private volatile DescriptionLOD loadedDescriptionLoD = getDescLOD().withRelativeDetail(relativeDetail); - private DescriptionLOD next = loadedDescriptionLoD; - - @Override - protected List call() throws Exception { - do { - loadedDescriptionLoD = next; - if (loadedDescriptionLoD == getEventBundle().getDescriptionLOD()) { - return Collections.emptyList(); - } - bundles = loadBundles(); - next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); - } while (bundles.size() == 1 && nonNull(next)); - - // return list of AbstractDetailViewNodes representing sub-bundles - return bundles.stream() - .map(AbstractDetailViewNode.this::getNodeForBundle) - .collect(Collectors.toList()); - } - - private Collection loadBundles() { - return makeBundlesFromClusters(eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD))); - } - - @Override - protected void succeeded() { - chart.setCursor(Cursor.WAIT); - try { - List subBundleNodes = get(); - if (subBundleNodes.isEmpty()) { - showSpans(true); - } else { - showSpans(false); - } - descLOD.set(loadedDescriptionLoD); - //assign subNodes and request chart layout - subNodePane.getChildren().setAll(subBundleNodes); - 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); - } - } - - final double getLayoutXCompensation() { - return (getParentNode() != null ? getParentNode().getLayoutXCompensation() : 0) - + getBoundsInParent().getMinX(); - } - - @Override - public final void setDescriptionVisibility(DescriptionVisibility descrVis) { - this.descrVis = descrVis; - final int size = getEventBundle().getEventIDs().size(); - - switch (this.descrVis) { - case COUNT_ONLY: - descrLabel.setText(""); - countLabel.setText(String.valueOf(size)); - break; - case HIDDEN: - countLabel.setText(""); - descrLabel.setText(""); - break; - default: - case SHOWN: - String description = getEventBundle().getDescription(); - description = getParentNode() != null - ? " ..." + StringUtils.substringAfter(description, getParentNode().getDescription()) - : description; - descrLabel.setText(description); - countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS - break; - } - } - - abstract S getNodeForBundle(T bundle); - - /** - * event handler used for mouse events on {@link AggregateEventNode}s - */ - private class EventMouseHandler 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(AbstractDetailViewNode.this) == false) { - chart.selectedNodes.add(AbstractDetailViewNode.this); - } - } else if (t.isShortcutDown()) { - chart.selectedNodes.removeAll(AbstractDetailViewNode.this); - } else if (t.getClickCount() > 1) { - final DescriptionLOD next = descLOD.get().moreDetailed(); - if (next != null) { - loadSubBundles(DescriptionLOD.RelativeDetail.MORE); - - } - } else { - chart.selectedNodes.setAll(AbstractDetailViewNode.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(expandClusterAction)); - contextMenu.getItems().add(ActionUtils.createMenuItem(collapseClusterAction)); - - contextMenu.getItems().add(new SeparatorMenuItem()); - contextMenu.getItems().addAll(chartContextMenu.getItems()); - } - contextMenu.show(AbstractDetailViewNode.this, t.getScreenX(), t.getScreenY()); - t.consume(); - } - } - } - - private class ExpandClusterAction extends Action { - - ExpandClusterAction() { - super("Expand"); - - 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 { - - CollapseClusterAction() { - super("Collapse"); - - setGraphic(new ImageView(MINUS)); - setEventHandler((ActionEvent t) -> { - final DescriptionLOD previous = descLOD.get().lessDetailed(); - if (previous != null) { - loadSubBundles(DescriptionLOD.RelativeDetail.LESS); - } - }); - disabledProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD())); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java deleted file mode 100644 index 5ae2ab2e0b..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package org.sleuthkit.autopsy.timeline.ui.detailview; - -import java.util.Comparator; -import java.util.List; -import java.util.Set; -import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; - -/** - * - */ -public interface DetailViewNode> { - - public void setDescriptionVisibility(DescriptionVisibility get); - - public List getSubNodes(); - - public void setSpanWidths(List spanWidths); - - public void setDescriptionWidth(double max); - - public EventBundle getEventBundle(); - - /** - * apply the 'effect' to visually indicate highlighted nodes - * - * @param applied true to apply the highlight 'effect', false to remove it - */ - void applyHighlightEffect(boolean applied); - - public void applySelectionEffect(boolean applied); - - default String getDescription() { - return getEventBundle().getDescription(); - } - - default EventType getEventType() { - return getEventBundle().getEventType(); - } - - default Set getEventIDs() { - return getEventBundle().getEventIDs(); - } - - default public long getStartMillis() { - return getEventBundle().getStartMillis(); - } - - default long getEndMillis() { - return getEventBundle().getEndMillis(); - } - - - - static class StartTimeComparator implements Comparator> { - - @Override - public int compare(DetailViewNode o1, DetailViewNode o2) { - return Long.compare(o1.getStartMillis(), o2.getStartMillis()); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index d4b932f296..906d93ffad 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -99,7 +99,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; * TODO: refactor common code out of this class and CountsChartPane into * {@link AbstractVisualization} */ -public class DetailViewPane extends AbstractVisualization, EventDetailChart> { +public class DetailViewPane extends AbstractVisualization { private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); @@ -119,7 +119,7 @@ public class DetailViewPane extends AbstractVisualization aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); public ObservableList getAggregatedEvents() { return aggregatedEvents; @@ -148,7 +148,7 @@ public class DetailViewPane extends AbstractVisualization> change) -> { + highlightedNodes.addListener((ListChangeListener.Change change) -> { while (change.next()) { change.getAddedSubList().forEach(aeNode -> { aeNode.applyHighlightEffect(true); @@ -211,8 +211,8 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); selectedNodes.stream().forEach((tn) -> { - for (DetailViewNode n : chart.getNodes((DetailViewNode t) -> - t.getDescription().equals(tn.getDescription()))) { + for (EventStripeNode n : chart.getNodes((EventStripeNode t) -> + t.getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); } }); @@ -235,8 +235,8 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); for (TreeItem tn : treeSelectionModel.getSelectedItems()) { - for (DetailViewNode n : chart.getNodes((DetailViewNode t) -> - t.getDescription().equals(tn.getValue().getDescription()))) { + for (EventStripeNode n : chart.getNodes((EventStripeNode t) -> + t.getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); } } @@ -356,7 +356,7 @@ public class DetailViewPane extends AbstractVisualization c1, Boolean selected) { + protected void applySelectionEffect(EventStripeNode c1, Boolean selected) { chart.applySelectionEffect(c1, selected); } @@ -422,7 +422,7 @@ 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; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Level; -import java.util.stream.Collectors; -import javafx.beans.property.SimpleObjectProperty; -import javafx.geometry.Pos; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.Tooltip; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Region; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.ColorUtilities; -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.TimeLineEvent; -import org.sleuthkit.datamodel.TskCoreException; - -/** - * Represents an {@link EventCluster} in a {@link EventDetailChart}. - */ -public class EventClusterNode extends AbstractDetailViewNode { - - private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); - - /** - * the region that represents the time span of this node's event - */ - private final Region spanRegion = new Region(); - - /** - * the context menu that with the slider that controls subnode/event display - * - * //TODO: move more of the control of subnodes/events here and out of - * EventDetail Chart - */ - private final SimpleObjectProperty contextMenu = new SimpleObjectProperty<>(); - - private Tooltip tooltip; - - public EventClusterNode(final EventCluster eventCluster, EventClusterNode parentEventNode, EventDetailChart chart) { - super(chart, eventCluster, parentEventNode); - minWidthProperty().bind(spanRegion.widthProperty()); - header.setPrefWidth(USE_COMPUTED_SIZE); - - final BorderPane borderPane = new BorderPane(getSubNodePane(), header, null, null, null); - BorderPane.setAlignment(getSubNodePane(), Pos.TOP_LEFT); - borderPane.setPrefWidth(USE_COMPUTED_SIZE); - - getChildren().addAll(spanRegion, borderPane); - - //setup backgrounds - spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS - spanRegion.setBackground(getBackground()); - - } - - @Override - synchronized void installTooltip() { - //TODO: all this work should probably go on a background thread... - if (tooltip == null) { - HashMap hashSetCounts = new HashMap<>(); - if (!getEventCluster().getEventIDsWithHashHits().isEmpty()) { - hashSetCounts = new HashMap<>(); - try { - for (TimeLineEvent tle : getEventsModel().getEventsById(getEventCluster().getEventIDsWithHashHits())) { - Set hashSetNames = getSleuthkitCase().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 (!getEventCluster().getEventIDsWithTags().isEmpty()) { - tagCounts.putAll(getEventsModel().getTagCountsByTagName(getEventCluster().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")); - - tooltip = new Tooltip( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text", - getEventCluster().getEventIDs().size(), getEventCluster().getEventType(), getEventCluster().getDescription(), - getEventCluster().getSpan().getStart().toString(TimeLineController.getZonedFormatter()), - getEventCluster().getSpan().getEnd().toString(TimeLineController.getZonedFormatter())) - + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) - + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString) - ); - Tooltip.install(EventClusterNode.this, tooltip); - } - } - - synchronized public EventCluster getEventCluster() { - return getEventBundle(); - } - - /** - * sets the width of the {@link Region} with border and background used to - * indicate the temporal span of this aggregate event - * - * @param w - */ - private void setSpanWidth(double w) { - spanRegion.setPrefWidth(w); - spanRegion.setMaxWidth(w); - spanRegion.setMinWidth(Math.max(2, w)); - } - - @Override - public void setSpanWidths(List spanWidths) { - setSpanWidth(spanWidths.get(0)); - - } - - @Override - Region getSpanFillNode() { - return spanRegion; - } - - /** - * @return the contextMenu - */ - public ContextMenu getContextMenu() { - return contextMenu.get(); - } - - /** - * @param contextMenu the contextMenu to set - */ - public void setContextMenu(ContextMenu contextMenu) { - this.contextMenu.set(contextMenu); - } - - @Override - void showSpans(boolean showSpans) { - //no-op for now - } - - @Override - Collection makeBundlesFromClusters(List eventClusters) { - return eventClusters; - } - - @Override - EventClusterNode getNodeForBundle(EventCluster cluster) { - return new EventClusterNode(cluster, this, getChart()); - } - -} 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 3d7f7bc017..076436a284 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -29,7 +29,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.MissingResourceException; -import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -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; @@ -52,7 +50,6 @@ import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Cursor; import javafx.scene.Group; -import javafx.scene.Node; import javafx.scene.chart.Axis; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; @@ -152,10 +149,6 @@ public final class EventDetailChart extends XYChart impl */ private final Group nodeGroup = new Group(); - /** - * map from event to node - */ - private final Map clusterNodeMap = new HashMap<>(); private final Map, EventStripe> stripeDescMap = new HashMap<>(); private final Map stripeNodeMap = new HashMap<>(); /** @@ -173,7 +166,7 @@ public final class EventDetailChart extends XYChart impl @GuardedBy(value = "this") private boolean requiresLayout = true; - final ObservableList> selectedNodes; + final ObservableList selectedNodes; /** * list of series of data added to this chart TODO: replace this with a map @@ -201,9 +194,8 @@ public final class EventDetailChart extends XYChart impl * via slider if truncateAll is true */ 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,18 +247,18 @@ public final class EventDetailChart extends XYChart impl this.selectedNodes = selectedNodes; this.selectedNodes.addListener(( - ListChangeListener.Change> c) -> { + ListChangeListener.Change c) -> { while (c.next()) { - c.getRemoved().forEach((DetailViewNode t) -> { - t.getEventBundle().getRanges().forEach((Range t1) -> { + c.getRemoved().forEach((EventStripeNode t) -> { + t.getEventStripe().getRanges().forEach((Range t1) -> { Line removedLine = projectionMap.remove(t1); getChartChildren().removeAll(removedLine); }); }); - c.getAddedSubList().forEach((DetailViewNode t) -> { + c.getAddedSubList().forEach((EventStripeNode t) -> { - for (Range range : t.getEventBundle().getRanges()) { + for (Range range : t.getEventStripe().getRanges()) { 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 @@ -281,7 +273,7 @@ public final class EventDetailChart extends XYChart impl } this.controller.selectEventIDs(selectedNodes.stream() - .flatMap(detailNode -> detailNode.getEventIDs().stream()) + .flatMap(detailNode -> detailNode.getEventsIDs().stream()) .collect(Collectors.toList())); }); @@ -410,30 +402,21 @@ public final class EventDetailChart extends XYChart impl @Override protected synchronized void dataItemAdded(Series series, int i, Data data) { - final EventCluster aggEvent = data.getYValue(); - if (alternateLayout.get()) { - EventStripe eventCluster = stripeDescMap.merge(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription()), - new EventStripe(aggEvent), - (EventStripe u, EventStripe v) -> { - EventStripeNode remove = stripeNodeMap.remove(u); - nodeGroup.getChildren().remove(remove); - remove = stripeNodeMap.remove(v); - nodeGroup.getChildren().remove(remove); - return EventStripe.merge(u, v); - } - ); - EventStripeNode clusterNode = new EventStripeNode(eventCluster, null, EventDetailChart.this); - stripeNodeMap.put(eventCluster, clusterNode); - nodeGroup.getChildren().add(clusterNode); - } else { - clusterNodeMap.computeIfAbsent(aggEvent, (EventCluster t) -> { - EventClusterNode eventNode = new EventClusterNode(aggEvent, null, EventDetailChart.this); - eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis()))); - clusterNodeMap.put(aggEvent, eventNode); - nodeGroup.getChildren().add(eventNode); - return eventNode; - }); - } + final EventCluster eventCluster = data.getYValue(); + + EventStripe eventStripe = stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()), + new EventStripe(eventCluster), + (EventStripe u, EventStripe v) -> { + EventStripeNode remove = stripeNodeMap.remove(u); + nodeGroup.getChildren().remove(remove); + remove = stripeNodeMap.remove(v); + nodeGroup.getChildren().remove(remove); + return EventStripe.merge(u, v); + } + ); + EventStripeNode clusterNode = new EventStripeNode(EventDetailChart.this, eventStripe, null); + stripeNodeMap.put(eventStripe, clusterNode); + nodeGroup.getChildren().add(clusterNode); } @Override @@ -444,12 +427,10 @@ public final class EventDetailChart extends XYChart impl @Override protected synchronized void dataItemRemoved(Data data, Series series) { - EventCluster aggEvent = data.getYValue(); - Node removedNode = clusterNodeMap.remove(aggEvent); - nodeGroup.getChildren().remove(removedNode); + EventCluster eventCluster = data.getYValue(); - EventStripe removedCluster = stripeDescMap.remove(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription())); - removedNode = stripeNodeMap.remove(removedCluster); + EventStripe removedCluster = stripeDescMap.remove(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription())); + EventStripeNode removedNode = stripeNodeMap.remove(removedCluster); nodeGroup.getChildren().remove(removedNode); data.setNode(null); @@ -486,36 +467,19 @@ public final class EventDetailChart extends XYChart impl maxY.set(0.0); if (bandByType.get() == false) { - if (alternateLayout.get() == true) { - List nodes = new ArrayList<>(stripeNodeMap.values()); - nodes.sort(Comparator.comparing(DetailViewNode::getStartMillis)); - layoutNodes(nodes, minY, 0); - } else { - List nodes = new ArrayList<>(clusterNodeMap.values()); - nodes.sort(Comparator.comparing(DetailViewNode::getStartMillis)); - layoutNodes(nodes, minY, 0); - } - + List nodes = new ArrayList<>(stripeNodeMap.values()); + nodes.sort(Comparator.comparing(EventStripeNode::getStartMillis)); + layoutNodes(nodes, minY, 0); } else { for (Series s : sortedSeriesList) { - if (alternateLayout.get() == true) { - List nodes = s.getData().stream() - .map(Data::getYValue) - .map(cluster -> stripeDescMap.get(ImmutablePair.of(cluster.getEventType(), cluster.getDescription()))) - .distinct() - .sorted(Comparator.comparing(EventStripe::getStartMillis)) - .map(stripeNodeMap::get) - .collect(Collectors.toList()); - layoutNodes(nodes, minY, 0); - } else { - List nodes = s.getData().stream() - .map(Data::getYValue) - .map(clusterNodeMap::get) - .filter(Objects::nonNull) - .sorted(Comparator.comparing(EventClusterNode::getStartMillis)) - .collect(Collectors.toList()); - layoutNodes(nodes, minY, 0); - } + List nodes = s.getData().stream() + .map(Data::getYValue) + .map(cluster -> stripeDescMap.get(ImmutablePair.of(cluster.getEventType(), cluster.getDescription()))) + .distinct() + .sorted(Comparator.comparing(EventStripe::getStartMillis)) + .map(stripeNodeMap::get) + .collect(Collectors.toList()); + layoutNodes(nodes, minY, 0); minY = maxY.get(); } } @@ -551,24 +515,21 @@ public final class EventDetailChart extends XYChart impl return maxY.getReadOnlyProperty(); } - Iterable> getNodes(Predicate> p) { - Collection> values = alternateLayout.get() - ? stripeNodeMap.values() - : clusterNodeMap.values(); - + Iterable getNodes(Predicate p) { + Collection values = stripeNodeMap.values(); //collapse tree of DetailViewNoeds to list and then filter on given predicate return values.stream() .flatMap(EventDetailChart::flatten) .filter(p).collect(Collectors.toList()); } - public static Stream> flatten(DetailViewNode node) { + public static Stream flatten(EventStripeNode node) { return Stream.concat( Stream.of(node), node.getSubNodes().stream().flatMap(EventDetailChart::flatten)); } - Iterable> getAllNodes() { + Iterable getAllNodes() { return getNodes(x -> true); } @@ -596,12 +557,13 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - private synchronized double layoutNodes(final Collection> nodes, final double minY, final double xOffset) { + private synchronized double layoutNodes(final Collection< 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 Map maxXatY = new HashMap<>(); double localMax = minY; //for each node lay size it and position it in first available slot - for (AbstractDetailViewNode node : nodes) { + for (EventStripeNode node : nodes) { node.setDescriptionVisibility(descrVisibility.get()); double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis())); @@ -610,48 +572,40 @@ public final class EventDetailChart extends XYChart impl double layoutNodesResultHeight = 0; double span = 0; - List> subNodes = node.getSubNodes(); + List subNodes = node.getSubNodes(); if (subNodes.isEmpty() == false) { - subNodes.sort(new DetailViewNode.StartTimeComparator()); + subNodes.sort(Comparator.comparing(EventStripeNode::getStartMillis)); layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition); } - if (alternateLayout.get() == false) { - double endX = getXAxis().getDisplayPosition(new DateTime(node.getEndMillis())) - xOffset; - span = endX - startX; - //size timespan border - node.setSpanWidths(Arrays.asList(span)); - } else { - - EventStripeNode stripeNode = (EventStripeNode) node; - List spanWidths = new ArrayList<>(); - double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));; - double x2; - Iterator> ranges = stripeNode.getStripe().getRanges().iterator(); - Range range = ranges.next(); - do { - x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); - double clusterSpan = x2 - x; - span += clusterSpan; - spanWidths.add(clusterSpan); - if (ranges.hasNext()) { - range = ranges.next(); - x = getXAxis().getDisplayPosition(new DateTime(range.lowerEndpoint())); - double gapSpan = x - x2; - span += gapSpan; - spanWidths.add(gapSpan); - if (ranges.hasNext() == false) { - x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); - clusterSpan = x2 - x; - span += clusterSpan; - spanWidths.add(clusterSpan); - } + List spanWidths = new ArrayList<>(); + double x = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis()));; + double x2; + Iterator> ranges = node.getStripe().getRanges().iterator(); + Range range = ranges.next(); + do { + x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); + double clusterSpan = x2 - x; + span += clusterSpan; + spanWidths.add(clusterSpan); + if (ranges.hasNext()) { + range = ranges.next(); + x = getXAxis().getDisplayPosition(new DateTime(range.lowerEndpoint())); + double gapSpan = x - x2; + span += gapSpan; + spanWidths.add(gapSpan); + if (ranges.hasNext() == false) { + x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); + clusterSpan = x2 - x; + span += clusterSpan; + spanWidths.add(clusterSpan); } + } - } while (ranges.hasNext()); + } while (ranges.hasNext()); + + node.setSpanWidths(spanWidths); - stripeNode.setSpanWidths(spanWidths); - } if (truncateAll.get()) { //if truncate option is selected limit width of description label node.setDescriptionWidth(Math.max(span, truncateWidth.get())); } else { //else set it unbounded @@ -744,10 +698,6 @@ public final class EventDetailChart extends XYChart impl return filteredEvents; } - Property alternateLayoutProperty() { - return alternateLayout; - } - private class DetailIntervalSelector extends IntervalSelector { public DetailIntervalSelector(double x, double height, Axis axis, TimeLineController controller) { @@ -783,7 +733,7 @@ public final class EventDetailChart extends XYChart impl super.requestChartLayout(); } - void applySelectionEffect(DetailViewNode c1, Boolean selected) { + void applySelectionEffect(EventStripeNode c1, Boolean selected) { c1.applySelectionEffect(selected); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 7b2ffb606a..b32888afdf 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -7,30 +7,152 @@ 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.ExecutionException; +import java.util.logging.Level; import java.util.stream.Collectors; +import javafx.application.Platform; +import javafx.beans.property.SimpleObjectProperty; +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; +import javafx.scene.control.Label; +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.Background; import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.Border; +import javafx.scene.layout.BorderStroke; +import javafx.scene.layout.BorderStrokeStyle; +import javafx.scene.layout.BorderWidths; +import javafx.scene.layout.CornerRadii; import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import static javafx.scene.layout.Region.USE_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.ColorUtilities; +import org.sleuthkit.autopsy.coreutils.LoggedTask; +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 static org.sleuthkit.autopsy.timeline.ui.detailview.AbstractDetailViewNode.show; +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; /** * */ -public class EventStripeNode extends AbstractDetailViewNode { +final public class EventStripeNode extends StackPane { private final HBox rangesHBox = new HBox(); + private final EventStripe eventStripe; + private Tooltip tooltip; + + 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(); + ImageView hashIV = new ImageView(HASH_PIN); + ImageView tagIV = new ImageView(TAG); + if (eventStripe.getEventIDsWithHashHits().isEmpty()) { + show(hashIV, false); + } + if (eventStripe.getEventIDsWithTags().isEmpty()) { + show(tagIV, false); + } + + expandClusterAction = new ExpandClusterAction(); + plusButton = ActionUtils.createButton(expandClusterAction, ActionUtils.ActionTextBehavior.HIDE); + configureLODButton(plusButton); + + collapseClusterAction = new CollapseClusterAction(); + minusButton = ActionUtils.createButton(collapseClusterAction, ActionUtils.ActionTextBehavior.HIDE); + configureLODButton(minusButton); + + HBox.setHgrow(spacer, Priority.ALWAYS); + header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, minusButton, plusButton); + + header.setMinWidth(USE_PREF_SIZE); + header.setPadding(new Insets(2, 5, 2, 5)); + header.setAlignment(Pos.CENTER_LEFT); + //setup description label + evtColor = getEventType().getColor(); + + eventTypeImageView.setImage(getEventType().getFXImage()); + descrLabel.setGraphic(eventTypeImageView); + descrLabel.setPrefWidth(USE_COMPUTED_SIZE); + descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); + descrLabel.setMouseTransparent(true); + + //set up subnode pane sizing contraints + subNodePane.setPrefHeight(USE_COMPUTED_SIZE); + subNodePane.setMinHeight(USE_PREF_SIZE); + subNodePane.setMinWidth(USE_PREF_SIZE); + subNodePane.setMaxHeight(USE_PREF_SIZE); + subNodePane.setMaxWidth(USE_PREF_SIZE); + subNodePane.setPickOnBounds(false); + + setAlignment(Pos.TOP_LEFT); + setMinHeight(24); + setPrefHeight(USE_COMPUTED_SIZE); + setMaxHeight(USE_PREF_SIZE); + setOnMouseClicked(new EventMouseHandler()); + + //set up mouse hover effect and tooltip + setOnMouseEntered((MouseEvent e) -> { + //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart + installTooltip(); + showDescriptionLoDControls(true); + toFront(); + }); + + setOnMouseExited((MouseEvent e) -> { + showDescriptionLoDControls(false); + }); + setCursor(Cursor.HAND); + + setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + + setLayoutX(getChart().getXAxis().getDisplayPosition(new DateTime(eventStripe.getStartMillis())) - getLayoutXCompensation()); - EventStripeNode(EventStripe eventStripe, EventStripeNode parentNode, EventDetailChart chart) { - super(chart, eventStripe, parentNode); minWidthProperty().bind(rangesHBox.widthProperty()); final VBox internalVBox = new VBox(header, getSubNodePane()); internalVBox.setAlignment(Pos.CENTER_LEFT); @@ -51,13 +173,15 @@ public class EventStripeNode extends AbstractDetailViewNode new DropShadow(10, eventType.getColor())); + getSpanFillNode().setEffect(showControls ? dropShadow : null); + show(minusButton, showControls); + show(plusButton, showControls); show(getSpacer(), showControls); } - @Override public void setSpanWidths(List spanWidths) { for (int i = 0; i < spanWidths.size(); i++) { Region spanRegion = (Region) rangesHBox.getChildren().get(i); @@ -69,40 +193,450 @@ public class EventStripeNode extends AbstractDetailViewNode makeBundlesFromClusters(List eventClusters) { - return eventClusters.stream().collect( - Collectors.toMap( - EventCluster::getDescription, //key - EventStripe::new, //value - EventStripe::merge)//merge method - ).values(); - } - /** * * @param showSpans the value of showSpans */ - @Override void showSpans(final boolean showSpans) { rangesHBox.setVisible(showSpans); } - @Override - void installTooltip() { -// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + synchronized void installTooltip() { + //TODO: all this work should probably go on a background thread... + if (tooltip == null) { + HashMap hashSetCounts = new HashMap<>(); + if (!getStripe().getEventIDsWithHashHits().isEmpty()) { + hashSetCounts = new HashMap<>(); + try { + for (TimeLineEvent tle : getEventsModel().getEventsById(getStripe().getEventIDsWithHashHits())) { + Set hashSetNames = getSleuthkitCase().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(getEventsModel().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")); + + tooltip = new Tooltip( + NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text", + getEventStripe().getEventIDs().size(), getEventStripe().getEventType(), getEventStripe().getDescription(), + getEventStripe().getStart().toString(TimeLineController.getZonedFormatter()), + getEventStripe().getEnd().toString(TimeLineController.getZonedFormatter())) + + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) + + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString) + ); + Tooltip.install(this, tooltip); + } } - @Override EventStripeNode getNodeForBundle(EventStripe cluster) { - return new EventStripeNode(cluster, this, getChart()); + return new EventStripeNode(getChart(), cluster, this); + } + + static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); + static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS + static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS + static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS + static final CornerRadii CORNER_RADII = new CornerRadii(3); + /** + * the border to apply when this node is 'selected' + */ + static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); + private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class + .getName()); + + 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 final Map dropShadowMap = new HashMap<>(); + final Color evtColor; + + private final EventStripeNode parentNode; + private DescriptionVisibility descrVis; + + EventType getEventType() { + return eventStripe.getEventType(); + } + + String getDescription() { + return eventStripe.getDescription(); + } + + long getStartMillis() { + return eventStripe.getStartMillis(); + } + + /** + * Pane that contains AggregateEventNodes of any 'subevents' if they are + * displayed + * + * //TODO: move more of the control of subnodes/events here and out of + * EventDetail Chart + */ + private final Pane subNodePane = new Pane(); + + Pane getSubNodePane() { + return subNodePane; + } + + /** + * The ImageView used to show the icon for this node's event's type + */ + private final ImageView eventTypeImageView = new ImageView(); + + /** + * The label used to display this node's event's description + */ + final Label descrLabel = new Label(); + + /** + * The label used to display this node's event count + */ + final Label countLabel = new Label(); + + private final EventDetailChart chart; + private final SleuthkitCase sleuthkitCase; + + SleuthkitCase getSleuthkitCase() { + return sleuthkitCase; + } + + FilteredEventsModel getEventsModel() { + return eventsModel; + } + private final FilteredEventsModel eventsModel; + + private final Button plusButton; + private final Button minusButton; + + private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + final HBox header; + + Region getSpacer() { + return spacer; + } + + private final Region spacer = new Region(); + + private final CollapseClusterAction collapseClusterAction; + private final ExpandClusterAction expandClusterAction; + + @SuppressWarnings("unchecked") + public List getSubNodes() { + return subNodePane.getChildrenUnmodifiable().stream() + .map(t -> (EventStripeNode) t) + .collect(Collectors.toList()); + } + + /** + * apply the 'effect' to visually indicate selection + * + * @param applied true to apply the selection 'effect', false to remove it + */ + public void applySelectionEffect(boolean applied) { + Platform.runLater(() -> { + if (applied) { + setBorder(selectionBorder); + } else { + setBorder(null); + } + }); + } + + /** + * 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(eventStripe.getDescriptionLOD(), eventStripe.getDescription()), + new TypeFilter(getEventType())); + return subClusterFilter; + } + + /** + * @param w the maximum width the description label should have + */ + public void setDescriptionWidth(double w) { + getDescrLabel().setMaxWidth(w); + } + + /** + * apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ + public synchronized void applyHighlightEffect(boolean applied) { + if (applied) { + getDescrLabel().setStyle("-fx-font-weight: bold;"); // NON-NLS + getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY))); + setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); + } else { + getDescrLabel().setStyle("-fx-font-weight: normal;"); // NON-NLS + getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + } + } + + String getDisplayedDescription() { + return getDescrLabel().getText(); + } + + Button getPlusButton() { + return plusButton; + } + + Button getMinusButton() { + return minusButton; + } + + public final Label getDescrLabel() { + return descrLabel; + } + + final public Label getCountLabel() { + return countLabel; + } + + public EventStripeNode getParentNode() { + return parentNode; + } + + public final EventDetailChart getChart() { + return chart; + } + + public DescriptionLOD getDescLOD() { + return descLOD.get(); + } + + /** + * loads sub-bundles at the given Description LOD, continues + * + * @param requestedDescrLoD + * @param expand + */ + private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) { + subNodePane.getChildren().clear(); + if (descLOD.get().withRelativeDetail(relativeDetail) == eventStripe.getDescriptionLOD()) { + descLOD.set(eventStripe.getDescriptionLOD()); + showSpans(true); + chart.setRequiresLayout(true); + chart.requestChartLayout(); + } else { + showSpans(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, getDescLOD()); + + LoggedTask> loggedTask = new LoggedTask>( + NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { + private Collection bundles; + private volatile DescriptionLOD loadedDescriptionLoD = getDescLOD().withRelativeDetail(relativeDetail); + private DescriptionLOD next = loadedDescriptionLoD; + + @Override + protected Set call() throws Exception { + do { + loadedDescriptionLoD = next; + if (loadedDescriptionLoD == eventStripe.getDescriptionLOD()) { + return Collections.emptySet(); + } + bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream() + .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.stream() + .map(EventStripeNode.this::getNodeForBundle) + .collect(Collectors.toSet()); + } + + @Override + protected void succeeded() { + chart.setCursor(Cursor.WAIT); + try { + Set subBundleNodes = get(); + if (subBundleNodes.isEmpty()) { + showSpans(true); + } else { + showSpans(false); + } + descLOD.set(loadedDescriptionLoD); + //assign subNodes and request chart layout + subNodePane.getChildren().setAll(subBundleNodes); + 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); + } + } + + double getLayoutXCompensation() { + return (getParentNode() != null ? getParentNode().getLayoutXCompensation() : 0) + + getBoundsInParent().getMinX(); + } + + public void setDescriptionVisibility(DescriptionVisibility descrVis) { + this.descrVis = descrVis; + final int size = eventStripe.getEventIDs().size(); + + switch (this.descrVis) { + case COUNT_ONLY: + descrLabel.setText(""); + countLabel.setText(String.valueOf(size)); + break; + case HIDDEN: + countLabel.setText(""); + descrLabel.setText(""); + break; + default: + case SHOWN: + String description = eventStripe.getDescription(); + description = getParentNode() != null + ? " ..." + StringUtils.substringAfter(description, getParentNode().getDescription()) + : description; + descrLabel.setText(description); + countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS + break; + } + } + + public EventStripe getEventStripe() { + return eventStripe; + } + + public Set getEventsIDs() { + return eventStripe.getEventIDs(); + } + + /** + * event handler used for mouse events on {@link AggregateEventNode}s + */ + private class EventMouseHandler 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(EventStripeNode.this) == false) { + chart.selectedNodes.add(EventStripeNode.this); + } + } 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); + } + 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(expandClusterAction)); + contextMenu.getItems().add(ActionUtils.createMenuItem(collapseClusterAction)); + + contextMenu.getItems().add(new SeparatorMenuItem()); + contextMenu.getItems().addAll(chartContextMenu.getItems()); + } + contextMenu.show(EventStripeNode.this, t.getScreenX(), t.getScreenY()); + t.consume(); + } + } + } + + private class ExpandClusterAction extends Action { + + ExpandClusterAction() { + super("Expand"); + + 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 { + + CollapseClusterAction() { + super("Collapse"); + + setGraphic(new ImageView(MINUS)); + setEventHandler((ActionEvent t) -> { + final DescriptionLOD previous = descLOD.get().lessDetailed(); + if (previous != null) { + loadSubBundles(DescriptionLOD.RelativeDetail.LESS); + } + }); + disabledProperty().bind(descLOD.isEqualTo(eventStripe.getDescriptionLOD())); + } } } 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 9971291863..57f2270f5b 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 @@ -43,7 +43,6 @@ import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineView; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewNode; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; /** @@ -91,8 +90,8 @@ public class NavPanel extends BorderPane implements TimeLineView { }); detailViewPane.getSelectedNodes().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); - detailViewPane.getSelectedNodes().forEach((DetailViewNode t) -> { - eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEventBundle())); + detailViewPane.getSelectedNodes().forEach(eventStripeNode -> { + eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(eventStripeNode.getEventStripe())); }); }); From eb0c72b43a614c71e15dde83f158ad6150fbc5d6 Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 29 Sep 2015 14:18:45 -0400 Subject: [PATCH 2/5] more cleanup and simplification of code for only new style clustering --- .../timeline/datamodel/EventStripe.java | 10 - .../timeline/ui/detailview/Bundle.properties | 2 - .../ui/detailview/DetailViewPane.java | 12 +- .../ui/detailview/EventDetailChart.java | 4 - .../ui/detailview/EventStripeNode.java | 570 +++++++++--------- 5 files changed, 285 insertions(+), 313 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index 4a3f86e4f2..e88e8ff623 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -15,7 +15,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; import javax.annotation.concurrent.Immutable; -import org.joda.time.DateTime; import org.python.google.common.base.Objects; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; @@ -141,13 +140,4 @@ public final class EventStripe implements EventBundle { public Iterable> getRanges() { return spans.asRanges(); } - - public DateTime getEnd() { - return spanMap.get(getStartMillis()).getSpan().getStart(); - } - - public DateTime getStart() { - return spanMap.get(getEndMillis()).getSpan().getStart(); - } - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties index 7b87db0380..f8c9b17c67 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties @@ -1,6 +1,4 @@ Timeline.ui.detailview.tooltip.text={0}\nRight-click to remove.\nRight-drag to reposition. -AggregateEventNode.installTooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4} -AggregateEventNode.loggedTask.name=Load sub events DetailViewPane.loggedTask.name=Update Details DetailViewPane.loggedTask.preparing=preparing DetailViewPane.loggedTask.queryDb=querying db 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 906d93ffad..4b038248e5 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -150,11 +150,11 @@ public class DetailViewPane extends AbstractVisualization change) -> { while (change.next()) { - change.getAddedSubList().forEach(aeNode -> { - aeNode.applyHighlightEffect(true); + change.getAddedSubList().forEach(node -> { + node.applyHighlightEffect(true); }); - change.getRemoved().forEach(aeNode -> { - aeNode.applyHighlightEffect(false); + change.getRemoved().forEach(node -> { + node.applyHighlightEffect(false); }); } }); @@ -357,7 +357,7 @@ public class DetailViewPane extends AbstractVisualization impl protected void requestChartLayout() { super.requestChartLayout(); } - - void applySelectionEffect(EventStripeNode c1, Boolean selected) { - c1.applySelectionEffect(selected); - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index b32888afdf..42e553abc8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -1,23 +1,33 @@ /* - * 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.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.ExecutionException; import java.util.logging.Level; import java.util.stream.Collectors; -import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; +import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; @@ -44,33 +54,23 @@ import javafx.scene.layout.BorderWidths; import javafx.scene.layout.CornerRadii; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import static javafx.scene.layout.Region.USE_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.ColorUtilities; -import org.sleuthkit.autopsy.coreutils.LoggedTask; 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; @@ -79,9 +79,77 @@ import org.sleuthkit.datamodel.TskCoreException; */ final public class EventStripeNode extends StackPane { + 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 = new CornerRadii(3); + + /** + * the border to apply when this node is selected + */ + private static final Border SELECTION_BORDER = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, 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 final HBox rangesHBox = new HBox(); private final EventStripe eventStripe; private Tooltip tooltip; + private final Map dropShadowMap = new HashMap<>(); + final Color evtColor; + + private final EventStripeNode parentNode; + private DescriptionVisibility descrVis; + + /** + * Pane that contains AggregateEventNodes of any 'subevents' if they are + * displayed + * + * //TODO: move more of the control of subnodes/events here and out of + * EventDetail Chart + */ + private final Pane subNodePane = new Pane(); + + /** + * The ImageView used to show the icon for this node's event's type + */ + private final ImageView eventTypeImageView = new ImageView(); + + /** + * The label used to display this node's event's description + */ + final Label descrLabel = new Label(); + + /** + * The label used to display this node's event count + */ + final Label countLabel = new Label(); + + private final EventDetailChart chart; + private final SleuthkitCase sleuthkitCase; + + private final FilteredEventsModel eventsModel; + + private final Button plusButton; + private final Button minusButton; + + private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + final HBox header; + + private final CollapseClusterAction collapseClusterAction; + private final ExpandClusterAction expandClusterAction; public EventStripeNode(EventDetailChart chart, EventStripe eventStripe, EventStripeNode parentEventNode) { this.eventStripe = eventStripe; @@ -101,14 +169,13 @@ final public class EventStripeNode extends StackPane { expandClusterAction = new ExpandClusterAction(); plusButton = ActionUtils.createButton(expandClusterAction, ActionUtils.ActionTextBehavior.HIDE); - configureLODButton(plusButton); + configureLoDButton(plusButton); collapseClusterAction = new CollapseClusterAction(); minusButton = ActionUtils.createButton(collapseClusterAction, ActionUtils.ActionTextBehavior.HIDE); - configureLODButton(minusButton); + configureLoDButton(minusButton); - HBox.setHgrow(spacer, Priority.ALWAYS); - header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, minusButton, plusButton); + header = new HBox(5, descrLabel, countLabel, hashIV, tagIV, minusButton, plusButton); header.setMinWidth(USE_PREF_SIZE); header.setPadding(new Insets(2, 5, 2, 5)); @@ -134,7 +201,7 @@ final public class EventStripeNode extends StackPane { setMinHeight(24); setPrefHeight(USE_COMPUTED_SIZE); setMaxHeight(USE_PREF_SIZE); - setOnMouseClicked(new EventMouseHandler()); + setOnMouseClicked(new MouseHandler()); //set up mouse hover effect and tooltip setOnMouseEntered((MouseEvent e) -> { @@ -151,10 +218,10 @@ final public class EventStripeNode extends StackPane { setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - setLayoutX(getChart().getXAxis().getDisplayPosition(new DateTime(eventStripe.getStartMillis())) - getLayoutXCompensation()); + setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventStripe.getStartMillis())) - getLayoutXCompensation()); minWidthProperty().bind(rangesHBox.widthProperty()); - final VBox internalVBox = new VBox(header, getSubNodePane()); + final VBox internalVBox = new VBox(header, subNodePane); internalVBox.setAlignment(Pos.CENTER_LEFT); for (Range range : eventStripe.getRanges()) { @@ -176,10 +243,9 @@ final public class EventStripeNode extends StackPane { void showDescriptionLoDControls(final boolean showControls) { DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), eventType -> new DropShadow(10, eventType.getColor())); - getSpanFillNode().setEffect(showControls ? dropShadow : null); + rangesHBox.setEffect(showControls ? dropShadow : null); show(minusButton, showControls); show(plusButton, showControls); - show(getSpacer(), showControls); } public void setSpanWidths(List spanWidths) { @@ -196,93 +262,73 @@ final public class EventStripeNode extends StackPane { return eventStripe; } - HBox getSpanFillNode() { - return rangesHBox; - } - - /** - * - * @param showSpans the value of showSpans - */ - void showSpans(final boolean showSpans) { - rangesHBox.setVisible(showSpans); - } - + @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}"}) synchronized void installTooltip() { - //TODO: all this work should probably go on a background thread... if (tooltip == null) { - HashMap hashSetCounts = new HashMap<>(); - if (!getStripe().getEventIDsWithHashHits().isEmpty()) { - hashSetCounts = new HashMap<>(); - try { - for (TimeLineEvent tle : getEventsModel().getEventsById(getStripe().getEventIDsWithHashHits())) { - Set hashSetNames = getSleuthkitCase().getAbstractFileById(tle.getFileID()).getHashSetNames(); - for (String hashSetName : hashSetNames) { - hashSetCounts.merge(hashSetName, 1L, Long::sum); + Task tooltTipTask = new Task() { + + @Override + protected String call() throws Exception { + HashMap hashSetCounts = new HashMap<>(); + if (!getStripe().getEventIDsWithHashHits().isEmpty()) { + hashSetCounts = new HashMap<>(); + try { + for (TimeLineEvent tle : eventsModel.getEventsById(getStripe().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); } } - } 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); } - } - Map tagCounts = new HashMap<>(); - if (getEventStripe().getEventIDsWithTags().isEmpty() == false) { - tagCounts.putAll(getEventsModel().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")); - - tooltip = new Tooltip( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text", - getEventStripe().getEventIDs().size(), getEventStripe().getEventType(), getEventStripe().getDescription(), - getEventStripe().getStart().toString(TimeLineController.getZonedFormatter()), - getEventStripe().getEnd().toString(TimeLineController.getZonedFormatter())) - + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) - + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString) - ); - Tooltip.install(this, tooltip); + @Override + protected void succeeded() { + super.succeeded(); + try { + tooltip = new Tooltip(get()); + Tooltip.install(this, tooltip); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex); + Tooltip.uninstall(this, tooltip); + tooltip = null; + } + } + }; + chart.getController().monitorTask(tooltTipTask); } } +} - EventStripeNode getNodeForBundle(EventStripe cluster) { - return new EventStripeNode(getChart(), cluster, this); +EventStripeNode getNodeForBundle(EventStripe cluster) { + return new EventStripeNode(chart, cluster, this); } - static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); - static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS - static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS - static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS - static final CornerRadii CORNER_RADII = new CornerRadii(3); - /** - * the border to apply when this node is 'selected' - */ - static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); - private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class - .getName()); - - 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 final Map dropShadowMap = new HashMap<>(); - final Color evtColor; - - private final EventStripeNode parentNode; - private DescriptionVisibility descrVis; - EventType getEventType() { return eventStripe.getEventType(); } @@ -295,83 +341,13 @@ final public class EventStripeNode extends StackPane { return eventStripe.getStartMillis(); } - /** - * Pane that contains AggregateEventNodes of any 'subevents' if they are - * displayed - * - * //TODO: move more of the control of subnodes/events here and out of - * EventDetail Chart - */ - private final Pane subNodePane = new Pane(); - - Pane getSubNodePane() { - return subNodePane; - } - - /** - * The ImageView used to show the icon for this node's event's type - */ - private final ImageView eventTypeImageView = new ImageView(); - - /** - * The label used to display this node's event's description - */ - final Label descrLabel = new Label(); - - /** - * The label used to display this node's event count - */ - final Label countLabel = new Label(); - - private final EventDetailChart chart; - private final SleuthkitCase sleuthkitCase; - - SleuthkitCase getSleuthkitCase() { - return sleuthkitCase; - } - - FilteredEventsModel getEventsModel() { - return eventsModel; - } - private final FilteredEventsModel eventsModel; - - private final Button plusButton; - private final Button minusButton; - - private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); - final HBox header; - - Region getSpacer() { - return spacer; - } - - private final Region spacer = new Region(); - - private final CollapseClusterAction collapseClusterAction; - private final ExpandClusterAction expandClusterAction; - @SuppressWarnings("unchecked") - public List getSubNodes() { + public List getSubNodes() { return subNodePane.getChildrenUnmodifiable().stream() .map(t -> (EventStripeNode) t) .collect(Collectors.toList()); } - /** - * apply the 'effect' to visually indicate selection - * - * @param applied true to apply the selection 'effect', false to remove it - */ - public void applySelectionEffect(boolean applied) { - Platform.runLater(() -> { - if (applied) { - setBorder(selectionBorder); - } else { - setBorder(null); - } - }); - } - /** * make a new filter intersecting the global filter with description and * type filters to restrict sub-clusters @@ -389,7 +365,16 @@ final public class EventStripeNode extends StackPane { * @param w the maximum width the description label should have */ public void setDescriptionWidth(double w) { - getDescrLabel().setMaxWidth(w); + descrLabel.setMaxWidth(w); + } + + /** + * 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); } /** @@ -399,45 +384,17 @@ final public class EventStripeNode extends StackPane { */ public synchronized void applyHighlightEffect(boolean applied) { if (applied) { - getDescrLabel().setStyle("-fx-font-weight: bold;"); // NON-NLS - getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY))); + descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS + rangesHBox.setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY))); setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); } else { - getDescrLabel().setStyle("-fx-font-weight: normal;"); // NON-NLS - getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS + rangesHBox.setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); } } - String getDisplayedDescription() { - return getDescrLabel().getText(); - } - - Button getPlusButton() { - return plusButton; - } - - Button getMinusButton() { - return minusButton; - } - - public final Label getDescrLabel() { - return descrLabel; - } - - final public Label getCountLabel() { - return countLabel; - } - - public EventStripeNode getParentNode() { - return parentNode; - } - - public final EventDetailChart getChart() { - return chart; - } - - public DescriptionLOD getDescLOD() { + private DescriptionLOD getDescriptionLoD() { return descLOD.get(); } @@ -447,15 +404,16 @@ final public class EventStripeNode extends StackPane { * @param requestedDescrLoD * @param expand */ - private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) { + @NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters") + private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) { subNodePane.getChildren().clear(); if (descLOD.get().withRelativeDetail(relativeDetail) == eventStripe.getDescriptionLOD()) { descLOD.set(eventStripe.getDescriptionLOD()); - showSpans(true); + rangesHBox.setVisible(true); chart.setRequiresLayout(true); chart.requestChartLayout(); } else { - showSpans(false); + rangesHBox.setVisible(false); // make new ZoomParams to query with final RootFilter subClusterFilter = getSubClusterFilter(); @@ -466,16 +424,16 @@ final public class EventStripeNode extends StackPane { */ final Interval subClusterSpan = new Interval(eventStripe.getStartMillis(), eventStripe.getEndMillis() + 1000); final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); - final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescLOD()); + final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); LoggedTask> loggedTask = new LoggedTask>( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { + EventStripeNode_loggedTask_name(), true) { private Collection bundles; - private volatile DescriptionLOD loadedDescriptionLoD = getDescLOD().withRelativeDetail(relativeDetail); + private volatile DescriptionLOD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); private DescriptionLOD next = loadedDescriptionLoD; @Override - protected Set call() throws Exception { + protected Set call() throws Exception { do { loadedDescriptionLoD = next; if (loadedDescriptionLoD == eventStripe.getDescriptionLOD()) { @@ -497,14 +455,14 @@ final public class EventStripeNode extends StackPane { } @Override - protected void succeeded() { + protected void succeeded() { chart.setCursor(Cursor.WAIT); try { Set subBundleNodes = get(); if (subBundleNodes.isEmpty()) { - showSpans(true); + rangesHBox.setVisible(true); } else { - showSpans(false); + rangesHBox.setVisible(false); } descLOD.set(loadedDescriptionLoD); //assign subNodes and request chart layout @@ -523,8 +481,8 @@ final public class EventStripeNode extends StackPane { } } - double getLayoutXCompensation() { - return (getParentNode() != null ? getParentNode().getLayoutXCompensation() : 0) + private double getLayoutXCompensation() { + return (parentNode != null ? parentNode.getLayoutXCompensation() : 0) + getBoundsInParent().getMinX(); } @@ -533,19 +491,19 @@ final public class EventStripeNode extends StackPane { final int size = eventStripe.getEventIDs().size(); switch (this.descrVis) { - case COUNT_ONLY: - descrLabel.setText(""); - countLabel.setText(String.valueOf(size)); - break; case HIDDEN: countLabel.setText(""); descrLabel.setText(""); break; + case COUNT_ONLY: + descrLabel.setText(""); + countLabel.setText(String.valueOf(size)); + break; default: case SHOWN: String description = eventStripe.getDescription(); - description = getParentNode() != null - ? " ..." + StringUtils.substringAfter(description, getParentNode().getDescription()) + description = parentNode != null + ? " ..." + StringUtils.substringAfter(description, parentNode.getDescription()) : description; descrLabel.setText(description); countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS @@ -557,86 +515,118 @@ final public class EventStripeNode extends StackPane { return eventStripe; } - public Set getEventsIDs() { + Set getEventsIDs() { return eventStripe.getEventIDs(); - } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} /** - * event handler used for mouse events on {@link AggregateEventNode}s + * event handler used for mouse events on {@link EventStripeNode}s */ - private class EventMouseHandler implements EventHandler { + private class MouseHandler implements EventHandler { - private ContextMenu contextMenu; + private ContextMenu contextMenu; - @Override - public void handle(MouseEvent t) { + @Override + public void handle(MouseEvent t) { - if (t.getButton() == MouseButton.PRIMARY) { - t.consume(); - if (t.isShiftDown()) { - if (chart.selectedNodes.contains(EventStripeNode.this) == false) { - chart.selectedNodes.add(EventStripeNode.this); - } - } 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); + if (t.getButton() == MouseButton.PRIMARY) { + t.consume(); + if (t.isShiftDown()) { + if (chart.selectedNodes.contains(EventStripeNode.this) == false) { + chart.selectedNodes.add(EventStripeNode.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(expandClusterAction)); - contextMenu.getItems().add(ActionUtils.createMenuItem(collapseClusterAction)); - - contextMenu.getItems().add(new SeparatorMenuItem()); - contextMenu.getItems().addAll(chartContextMenu.getItems()); - } - contextMenu.show(EventStripeNode.this, t.getScreenX(), t.getScreenY()); - t.consume(); - } - } - } - - private class ExpandClusterAction extends Action { - - ExpandClusterAction() { - super("Expand"); - - setGraphic(new ImageView(PLUS)); - setEventHandler((ActionEvent t) -> { + } 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); } - }); - disabledProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); - } - } + } else { + chart.selectedNodes.setAll(EventStripeNode.this); + } + t.consume(); + } else if (t.getButton() == MouseButton.SECONDARY) { + ContextMenu chartContextMenu = chart.getChartContextMenu(t); + if (contextMenu == null) { + contextMenu = new ContextMenu(); + contextMenu.setAutoHide(true); - private class CollapseClusterAction extends Action { + contextMenu.getItems().add(ActionUtils.createMenuItem(expandClusterAction)); + contextMenu.getItems().add(ActionUtils.createMenuItem(collapseClusterAction)); - CollapseClusterAction() { - super("Collapse"); - - setGraphic(new ImageView(MINUS)); - setEventHandler((ActionEvent t) -> { - final DescriptionLOD previous = descLOD.get().lessDetailed(); - if (previous != null) { - loadSubBundles(DescriptionLOD.RelativeDetail.LESS); - } - }); - disabledProperty().bind(descLOD.isEqualTo(eventStripe.getDescriptionLOD())); + contextMenu.getItems().add(new SeparatorMenuItem()); + contextMenu.getItems().addAll(chartContextMenu.getItems()); + } + contextMenu.show(EventStripeNode.this, t.getScreenX(), t.getScreenY()); + t.consume(); } } } + +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(descLOD.isEqualTo(eventStripe.getDescriptionLOD())); + } +} +} From d8c78b56fa6963f463eed369284089ee27a25d1e Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 29 Sep 2015 15:14:43 -0400 Subject: [PATCH 3/5] load tooltip info on background thread --- .../ui/detailview/EventStripeNode.java | 197 +++++++++--------- 1 file changed, 95 insertions(+), 102 deletions(-) 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 42e553abc8..1dacc2c9dd 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -19,13 +19,17 @@ 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.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.event.ActionEvent; @@ -59,18 +63,28 @@ 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.ColorUtilities; +import org.sleuthkit.autopsy.coreutils.LoggedTask; 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 static org.sleuthkit.autopsy.timeline.ui.detailview.Bundle.EventStripeNode_loggedTask_name; 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; @@ -270,7 +284,8 @@ final public class EventStripeNode extends StackPane { "EventStripeNode.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}"}) synchronized void installTooltip() { if (tooltip == null) { - Task tooltTipTask = new Task() { + setCursor(Cursor.WAIT); + final Task tooltTipTask = new Task() { @Override protected String call() throws Exception { @@ -312,20 +327,27 @@ final public class EventStripeNode extends StackPane { super.succeeded(); try { tooltip = new Tooltip(get()); - Tooltip.install(this, tooltip); + Tooltip.install(EventStripeNode.this, tooltip); } catch (InterruptedException | ExecutionException ex) { LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex); - Tooltip.uninstall(this, tooltip); + Tooltip.uninstall(EventStripeNode.this, tooltip); tooltip = null; } } }; + + tooltTipTask.stateProperty().addListener((Observable observable) -> { + if (tooltTipTask.isDone()) { + setCursor(null); + } + }); + chart.getController().monitorTask(tooltTipTask); } } -} -EventStripeNode getNodeForBundle(EventStripe cluster) { + EventStripeNode getNodeForBundle(EventStripe cluster + ) { return new EventStripeNode(chart, cluster, this); } @@ -342,7 +364,7 @@ EventStripeNode getNodeForBundle(EventStripe cluster) { } @SuppressWarnings("unchecked") - public List getSubNodes() { + public List getSubNodes() { return subNodePane.getChildrenUnmodifiable().stream() .map(t -> (EventStripeNode) t) .collect(Collectors.toList()); @@ -405,7 +427,7 @@ EventStripeNode getNodeForBundle(EventStripe cluster) { * @param expand */ @NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters") - private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) { + private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) { subNodePane.getChildren().clear(); if (descLOD.get().withRelativeDetail(relativeDetail) == eventStripe.getDescriptionLOD()) { descLOD.set(eventStripe.getDescriptionLOD()); @@ -433,7 +455,7 @@ EventStripeNode getNodeForBundle(EventStripe cluster) { private DescriptionLOD next = loadedDescriptionLoD; @Override - protected Set call() throws Exception { + protected Set call() throws Exception { do { loadedDescriptionLoD = next; if (loadedDescriptionLoD == eventStripe.getDescriptionLOD()) { @@ -455,7 +477,7 @@ EventStripeNode getNodeForBundle(EventStripe cluster) { } @Override - protected void succeeded() { + protected void succeeded() { chart.setCursor(Cursor.WAIT); try { Set subBundleNodes = get(); @@ -517,116 +539,87 @@ EventStripeNode getNodeForBundle(EventStripe cluster) { Set getEventsIDs() { return eventStripe.getEventIDs(); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -} + } /** * event handler used for mouse events on {@link EventStripeNode}s */ private class MouseHandler implements EventHandler { - private ContextMenu contextMenu; + private ContextMenu contextMenu; - @Override - public void handle(MouseEvent t) { + @Override + public void handle(MouseEvent t) { - if (t.getButton() == MouseButton.PRIMARY) { - t.consume(); - if (t.isShiftDown()) { - if (chart.selectedNodes.contains(EventStripeNode.this) == false) { - chart.selectedNodes.add(EventStripeNode.this); + if (t.getButton() == MouseButton.PRIMARY) { + t.consume(); + if (t.isShiftDown()) { + if (chart.selectedNodes.contains(EventStripeNode.this) == false) { + chart.selectedNodes.add(EventStripeNode.this); + } + } 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); } - } else if (t.isShortcutDown()) { - chart.selectedNodes.removeAll(EventStripeNode.this); - } else if (t.getClickCount() > 1) { + 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(expandClusterAction)); + contextMenu.getItems().add(ActionUtils.createMenuItem(collapseClusterAction)); + + contextMenu.getItems().add(new SeparatorMenuItem()); + contextMenu.getItems().addAll(chartContextMenu.getItems()); + } + contextMenu.show(EventStripeNode.this, t.getScreenX(), t.getScreenY()); + t.consume(); + } + } + } + + 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); } - } else { - chart.selectedNodes.setAll(EventStripeNode.this); - } - t.consume(); - } else if (t.getButton() == MouseButton.SECONDARY) { - ContextMenu chartContextMenu = chart.getChartContextMenu(t); - if (contextMenu == null) { - contextMenu = new ContextMenu(); - contextMenu.setAutoHide(true); + }); + disabledProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); + } + } - contextMenu.getItems().add(ActionUtils.createMenuItem(expandClusterAction)); - contextMenu.getItems().add(ActionUtils.createMenuItem(collapseClusterAction)); + private class CollapseClusterAction extends Action { - contextMenu.getItems().add(new SeparatorMenuItem()); - contextMenu.getItems().addAll(chartContextMenu.getItems()); - } - contextMenu.show(EventStripeNode.this, t.getScreenX(), t.getScreenY()); - t.consume(); + @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(descLOD.isEqualTo(eventStripe.getDescriptionLOD())); } } } - -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(descLOD.isEqualTo(eventStripe.getDescriptionLOD())); - } -} -} From ef1bce95f204ff9b35235714dcc43ea53836fd61 Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 29 Sep 2015 16:11:39 -0400 Subject: [PATCH 4/5] clean up and rearange initialization code and tooltip loading --- .../ui/detailview/EventStripeNode.java | 256 ++++++++---------- 1 file changed, 113 insertions(+), 143 deletions(-) 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 1dacc2c9dd..72a6a8a4e9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -26,10 +26,10 @@ 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.Observable; import javafx.beans.property.SimpleObjectProperty; import javafx.concurrent.Task; import javafx.event.ActionEvent; @@ -69,8 +69,6 @@ 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.ColorUtilities; -import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; @@ -89,7 +87,7 @@ 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 { @@ -99,12 +97,11 @@ final public class EventStripeNode extends StackPane { 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 = new CornerRadii(3); - - /** - * the border to apply when this node is selected - */ - private static final Border SELECTION_BORDER = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); + 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))); static void configureLoDButton(Button b) { b.setMinSize(16, 16); @@ -118,53 +115,36 @@ final public class EventStripeNode extends StackPane { b.setManaged(show); } - private final HBox rangesHBox = new HBox(); - private final EventStripe eventStripe; - private Tooltip tooltip; - private final Map dropShadowMap = new HashMap<>(); - final Color evtColor; - - private final EventStripeNode parentNode; + private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); private DescriptionVisibility descrVis; + private Tooltip tooltip; /** - * Pane that contains AggregateEventNodes of any 'subevents' if they are + * Pane that contains EventStripeNodes for any 'subevents' if they are * displayed * * //TODO: move more of the control of subnodes/events here and out of * EventDetail Chart */ private final Pane subNodePane = new Pane(); - - /** - * The ImageView used to show the icon for this node's event's type - */ + 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); - /** - * The label used to display this node's event's description - */ - final Label descrLabel = new Label(); - - /** - * The label used to display this node's event count - */ - final Label countLabel = new Label(); - + private final 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 plusButton; - private final Button minusButton; - - private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); - final HBox header; - - private final CollapseClusterAction collapseClusterAction; - private final ExpandClusterAction expandClusterAction; - public EventStripeNode(EventDetailChart chart, EventStripe eventStripe, EventStripeNode parentEventNode) { this.eventStripe = eventStripe; this.parentNode = parentEventNode; @@ -172,33 +152,36 @@ final public class EventStripeNode extends StackPane { descLOD.set(eventStripe.getDescriptionLOD()); sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); eventsModel = chart.getController().getEventsModel(); - ImageView hashIV = new ImageView(HASH_PIN); - ImageView tagIV = new ImageView(TAG); + 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()); + if (eventStripe.getEventIDsWithHashHits().isEmpty()) { show(hashIV, false); } if (eventStripe.getEventIDsWithTags().isEmpty()) { show(tagIV, false); } - - expandClusterAction = new ExpandClusterAction(); - plusButton = ActionUtils.createButton(expandClusterAction, ActionUtils.ActionTextBehavior.HIDE); configureLoDButton(plusButton); - - collapseClusterAction = new CollapseClusterAction(); - minusButton = ActionUtils.createButton(collapseClusterAction, ActionUtils.ActionTextBehavior.HIDE); configureLoDButton(minusButton); - header = new HBox(5, descrLabel, countLabel, hashIV, tagIV, minusButton, plusButton); - - header.setMinWidth(USE_PREF_SIZE); - header.setPadding(new Insets(2, 5, 2, 5)); - header.setAlignment(Pos.CENTER_LEFT); + //initialize info hbox + infoHBox.setMinWidth(USE_PREF_SIZE); + infoHBox.setPadding(new Insets(2, 5, 2, 5)); + infoHBox.setAlignment(Pos.CENTER_LEFT); //setup description label - evtColor = getEventType().getColor(); eventTypeImageView.setImage(getEventType().getFXImage()); - descrLabel.setGraphic(eventTypeImageView); descrLabel.setPrefWidth(USE_COMPUTED_SIZE); descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); descrLabel.setMouseTransparent(true); @@ -211,15 +194,29 @@ final public class EventStripeNode extends StackPane { subNodePane.setMaxWidth(USE_PREF_SIZE); subNodePane.setPickOnBounds(false); - setAlignment(Pos.TOP_LEFT); - setMinHeight(24); - setPrefHeight(USE_COMPUTED_SIZE); - setMaxHeight(USE_PREF_SIZE); - setOnMouseClicked(new MouseHandler()); + 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()); + } + clustersHBox.getChildren().remove(clustersHBox.getChildren().size() - 1); + clustersHBox.setMaxWidth(USE_PREF_SIZE); + + final VBox internalVBox = new VBox(infoHBox, subNodePane); + internalVBox.setAlignment(Pos.CENTER_LEFT); + getChildren().addAll(clustersHBox, internalVBox); + + 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 + /* + * defer tooltip creation till needed, this had a surprisingly large + * impact on speed of loading the chart + */ installTooltip(); showDescriptionLoDControls(true); toFront(); @@ -228,26 +225,6 @@ final public class EventStripeNode extends StackPane { setOnMouseExited((MouseEvent e) -> { showDescriptionLoDControls(false); }); - setCursor(Cursor.HAND); - - setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - - setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventStripe.getStartMillis())) - getLayoutXCompensation()); - - minWidthProperty().bind(rangesHBox.widthProperty()); - final VBox internalVBox = new VBox(header, subNodePane); - internalVBox.setAlignment(Pos.CENTER_LEFT); - - for (Range range : eventStripe.getRanges()) { - Region rangeRegion = new Region(); - rangeRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS - rangeRegion.setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); - rangesHBox.getChildren().addAll(rangeRegion, new Region()); - } - rangesHBox.getChildren().remove(rangesHBox.getChildren().size() - 1); - rangesHBox.setMaxWidth(USE_PREF_SIZE); - setMaxWidth(USE_PREF_SIZE); - getChildren().addAll(rangesHBox, internalVBox); } /** @@ -257,14 +234,14 @@ final public class EventStripeNode extends StackPane { void showDescriptionLoDControls(final boolean showControls) { DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), eventType -> new DropShadow(10, eventType.getColor())); - rangesHBox.setEffect(showControls ? dropShadow : null); + clustersHBox.setEffect(showControls ? dropShadow : null); show(minusButton, showControls); show(plusButton, showControls); } public void setSpanWidths(List spanWidths) { for (int i = 0; i < spanWidths.size(); i++) { - Region spanRegion = (Region) rangesHBox.getChildren().get(i); + Region spanRegion = (Region) clustersHBox.getChildren().get(i); Double w = spanWidths.get(i); spanRegion.setPrefWidth(w); spanRegion.setMaxWidth(w); @@ -284,7 +261,6 @@ final public class EventStripeNode extends StackPane { "EventStripeNode.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}"}) synchronized void installTooltip() { if (tooltip == null) { - setCursor(Cursor.WAIT); final Task tooltTipTask = new Task() { @Override @@ -336,18 +312,11 @@ final public class EventStripeNode extends StackPane { } }; - tooltTipTask.stateProperty().addListener((Observable observable) -> { - if (tooltTipTask.isDone()) { - setCursor(null); - } - }); - chart.getController().monitorTask(tooltTipTask); } } - EventStripeNode getNodeForBundle(EventStripe cluster - ) { + EventStripeNode getNodeForBundle(EventStripe cluster) { return new EventStripeNode(chart, cluster, this); } @@ -407,12 +376,10 @@ final public class EventStripeNode extends StackPane { public synchronized void applyHighlightEffect(boolean applied) { if (applied) { descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS - rangesHBox.setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY))); - setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); + setBackground(highlightedBackground); } else { descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS - rangesHBox.setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + setBackground(defaultBackground); } } @@ -431,11 +398,11 @@ final public class EventStripeNode extends StackPane { subNodePane.getChildren().clear(); if (descLOD.get().withRelativeDetail(relativeDetail) == eventStripe.getDescriptionLOD()) { descLOD.set(eventStripe.getDescriptionLOD()); - rangesHBox.setVisible(true); + clustersHBox.setVisible(true); chart.setRequiresLayout(true); chart.requestChartLayout(); } else { - rangesHBox.setVisible(false); + clustersHBox.setVisible(false); // make new ZoomParams to query with final RootFilter subClusterFilter = getSubClusterFilter(); @@ -448,57 +415,61 @@ final public class EventStripeNode extends StackPane { final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); - LoggedTask> loggedTask = new LoggedTask>( - EventStripeNode_loggedTask_name(), true) { - private Collection bundles; - private volatile DescriptionLOD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); - private DescriptionLOD next = loadedDescriptionLoD; + Task> loggedTask = new Task>() { - @Override - protected Set call() throws Exception { - do { - loadedDescriptionLoD = next; - if (loadedDescriptionLoD == eventStripe.getDescriptionLOD()) { - return Collections.emptySet(); - } - bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream() + private volatile DescriptionLOD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); + + { + updateTitle(EventStripeNode_loggedTask_name()); + } + + @Override + protected Set 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() .collect(Collectors.toMap( EventCluster::getDescription, //key EventStripe::new, //value EventStripe::merge) //merge method ).values(); - next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); - } while (bundles.size() == 1 && nonNull(next)); + next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); + } while (bundles.size() == 1 && nonNull(next)); - // return list of AbstractEventStripeNodes representing sub-bundles - return bundles.stream() + // return list of AbstractEventStripeNodes representing sub-bundles + return bundles.stream() .map(EventStripeNode.this::getNodeForBundle) .collect(Collectors.toSet()); - } + } - @Override - protected void succeeded() { - chart.setCursor(Cursor.WAIT); - try { - Set subBundleNodes = get(); - if (subBundleNodes.isEmpty()) { - rangesHBox.setVisible(true); - } else { - rangesHBox.setVisible(false); - } - descLOD.set(loadedDescriptionLoD); - //assign subNodes and request chart layout - subNodePane.getChildren().setAll(subBundleNodes); - chart.setRequiresLayout(true); - chart.requestChartLayout(); - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); - } - chart.setCursor(null); + @Override + protected void succeeded() { + chart.setCursor(Cursor.WAIT); + try { + Set subBundleNodes = get(); + if (subBundleNodes.isEmpty()) { + clustersHBox.setVisible(true); + } else { + clustersHBox.setVisible(false); } - }; + descLOD.set(loadedDescriptionLoD); + //assign subNodes and request chart layout + subNodePane.getChildren().setAll(subBundleNodes); + chart.setRequiresLayout(true); + chart.requestChartLayout(); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); + } + chart.setCursor(null); + } + }; - //start task +//start task chart.getController().monitorTask(loggedTask); } } @@ -539,13 +510,12 @@ final public class EventStripeNode extends StackPane { Set getEventsIDs() { return eventStripe.getEventIDs(); - } /** * event handler used for mouse events on {@link EventStripeNode}s */ - private class MouseHandler implements EventHandler { + private class MouseClickHandler implements EventHandler { private ContextMenu contextMenu; @@ -576,8 +546,8 @@ final public class EventStripeNode extends StackPane { contextMenu = new ContextMenu(); contextMenu.setAutoHide(true); - contextMenu.getItems().add(ActionUtils.createMenuItem(expandClusterAction)); - contextMenu.getItems().add(ActionUtils.createMenuItem(collapseClusterAction)); + contextMenu.getItems().add(ActionUtils.createMenuItem(new ExpandClusterAction())); + contextMenu.getItems().add(ActionUtils.createMenuItem(new CollapseClusterAction())); contextMenu.getItems().add(new SeparatorMenuItem()); contextMenu.getItems().addAll(chartContextMenu.getItems()); From 6d7bd8281c8baac7607440709ffb9773e1bebef9 Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 29 Sep 2015 16:45:10 -0400 Subject: [PATCH 5/5] cleanup EventDetailChart --- .../timeline/ui/detailview/Bundle.properties | 2 - .../ui/detailview/DetailViewPane.java | 10 +- .../ui/detailview/EventDetailChart.java | 237 ++++++++---------- 3 files changed, 111 insertions(+), 138 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties index f8c9b17c67..091926228a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties @@ -3,8 +3,6 @@ DetailViewPane.loggedTask.name=Update Details DetailViewPane.loggedTask.preparing=preparing DetailViewPane.loggedTask.queryDb=querying db DetailViewPane.loggedTask.updateUI=updating ui -EventDetailChart.contextMenu.zoomHistory.name=Zoom History -EventDetailChart.chartContextMenu.placeMarker.name=Place Marker DetailViewPane.truncateSliderLabel.text=max description width (px)\: DetailViewPane.advancedLayoutOptionsButtonLabel.text=Advanced Layout Options DetailViewPane.bandByTypeBox.text=Band by Type 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 4b038248e5..46d6709285 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -145,7 +145,7 @@ public class DetailViewPane extends AbstractVisualization change) -> { @@ -226,7 +226,7 @@ public class DetailViewPane extends AbstractVisualization> selectionModel) { @@ -448,11 +448,11 @@ public class DetailViewPane extends AbstractVisualization { if (newToggle == countsRadio) { - chart.getDescrVisibility().set(DescriptionVisibility.COUNT_ONLY); + chart.descrVisibilityProperty().set(DescriptionVisibility.COUNT_ONLY); } else if (newToggle == showRadio) { - chart.getDescrVisibility().set(DescriptionVisibility.SHOWN); + chart.descrVisibilityProperty().set(DescriptionVisibility.SHOWN); } else if (newToggle == hiddenRadio) { - chart.getDescrVisibility().set(DescriptionVisibility.HIDDEN); + chart.descrVisibilityProperty().set(DescriptionVisibility.HIDDEN); } }); 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 d7643907a6..2f285f103a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -45,7 +45,6 @@ import javafx.beans.property.SimpleObjectProperty; 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; @@ -77,6 +76,8 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import static org.sleuthkit.autopsy.timeline.ui.detailview.Bundle.EventDetailChart_chartContextMenu_placeMarker_name; +import static org.sleuthkit.autopsy.timeline.ui.detailview.Bundle.EventDetailChart_contextMenu_zoomHistory_name; /** * Custom implementation of {@link XYChart} to graph events on a horizontal @@ -94,15 +95,10 @@ import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; */ public final class EventDetailChart extends XYChart implements TimeLineChart { + 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; - - /** - * true == layout each event type in its own band, false == mix all the - * events together during layout - */ - private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false); + private static final int DEFAULT_ROW_HEIGHT = 24; private ContextMenu chartContextMenu; @@ -110,11 +106,6 @@ public final class EventDetailChart extends XYChart impl private FilteredEventsModel filteredEvents; - /** - * how much detail of the description to show in the ui - */ - private final SimpleObjectProperty descrVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); - /** * a user position-able vertical line to help the compare events */ @@ -130,36 +121,17 @@ public final class EventDetailChart extends XYChart impl /** * listener that triggers layout pass */ - private final InvalidationListener layoutInvalidationListener = ( - Observable o) -> { - synchronized (EventDetailChart.this) { - requiresLayout = true; - requestChartLayout(); - } - }; + private final InvalidationListener layoutInvalidationListener = (Observable o) -> { + synchronized (EventDetailChart.this) { + requiresLayout = true; + requestChartLayout(); + } + }; /** * the maximum y value used so far during the most recent layout pass */ private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); - - /** - * 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 Map, EventStripe> stripeDescMap = new HashMap<>(); - private final Map stripeNodeMap = new HashMap<>(); - /** - * true == enforce that no two events can share the same 'row', leading to - * sparser but possibly clearer layout. false == put unrelated events in the - * same 'row', creating a denser more compact layout - */ - private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false); - - private final Map, Line> projectionMap = new HashMap<>(); - /** * flag indicating whether this chart actually needs a layout pass */ @@ -167,6 +139,14 @@ public final class EventDetailChart extends XYChart impl private boolean requiresLayout = true; 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 Map, EventStripe> stripeDescMap = new HashMap<>(); + private final Map stripeNodeMap = new HashMap<>(); + private final Map, Line> projectionMap = new HashMap<>(); /** * list of series of data added to this chart TODO: replace this with a map @@ -180,6 +160,23 @@ public final class EventDetailChart extends XYChart impl final List collect = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList()); return Integer.compare(collect.indexOf(s1.getName()), collect.indexOf(s2.getName())); }); + /** + * true == layout each event type in its own band, false == mix all the + * events together during layout + */ + private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false); + /** + * true == enforce that no two events can share the same 'row', leading to + * sparser but possibly clearer layout. false == put unrelated events in the + * same 'row', creating a denser more compact layout + */ + private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false); + + /** + * how much detail of the description to show in the ui + */ + private final SimpleObjectProperty descrVisibility + = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); /** * true == truncate all the labels to the greater of the size of their @@ -199,7 +196,7 @@ public final class EventDetailChart extends XYChart impl 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.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); @@ -210,17 +207,17 @@ public final class EventDetailChart extends XYChart impl //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly getPlotChildren().add(nodeGroup); - //bind listener to events that should trigger layout + //add listener for events that should trigger layout widthProperty().addListener(layoutInvalidationListener); heightProperty().addListener(layoutInvalidationListener); - bandByType.addListener(layoutInvalidationListener); oneEventPerRow.addListener(layoutInvalidationListener); truncateAll.addListener(layoutInvalidationListener); truncateWidth.addListener(layoutInvalidationListener); descrVisibility.addListener(layoutInvalidationListener); - //this is needed to allow non circular binding of the guideline and timerangRect heights to the height of the chart + //this is needed to allow non circular binding of the guideline and timerangeRect heights to the height of the chart + //TODO: seems like a hack, can we remove? -jm boundsInLocalProperty().addListener((Observable observable) -> { setPrefHeight(boundsInLocalProperty().get().getHeight()); }); @@ -280,37 +277,18 @@ public final class EventDetailChart extends XYChart impl requestChartLayout(); } + TimeLineController getController() { + return controller; + } + + @NbBundle.Messages({"EventDetailChart.chartContextMenu.placeMarker.name=Place Marker", + "EventDetailChart.contextMenu.zoomHistory.name=Zoom History"}) ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException { if (chartContextMenu != null) { chartContextMenu.hide(); } - chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new Action( - NbBundle.getMessage(this.getClass(), "EventDetailChart.chartContextMenu.placeMarker.name")) { - { - setGraphic(new ImageView(new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true))); // NON-NLS - setEventHandler((ActionEvent t) -> { - if (guideLine == null) { - guideLine = new GuideLine(0, 0, 0, getHeight(), getXAxis()); - - guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); - guideLine.endYProperty().bind(heightProperty().subtract(getXAxis().heightProperty().subtract(getXAxis().tickLengthProperty()))); - - getChartChildren().add(guideLine); - - guideLine.setOnMouseClicked((MouseEvent event) -> { - if (event.getButton() == MouseButton.SECONDARY) { - clearGuideLine(); - event.consume(); - } - }); - } else { - guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); - } - }); - } - - }, new ActionGroup( - NbBundle.getMessage(this.getClass(), "EventDetailChart.contextMenu.zoomHistory.name"), + chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(clickEvent), + new ActionGroup(EventDetailChart_contextMenu_zoomHistory_name(), new Back(controller), new Forward(controller)))); chartContextMenu.setAutoHide(true); @@ -383,21 +361,20 @@ public final class EventDetailChart extends XYChart impl getChartChildren().add(getIntervalSelector()); } - public synchronized SimpleBooleanProperty oneEventPerRowProperty() { + SimpleBooleanProperty oneEventPerRowProperty() { return oneEventPerRow; } - public synchronized SimpleBooleanProperty truncateAllProperty() { + SimpleDoubleProperty getTruncateWidth() { + return truncateWidth; + } + + SimpleBooleanProperty truncateAllProperty() { return truncateAll; } - synchronized void setEventOnePerRow(Boolean t1) { - oneEventPerRow.set(t1); - } - - synchronized void setTruncateAll(Boolean t1) { - truncateAll.set(t1); - + SimpleObjectProperty< DescriptionVisibility> descrVisibilityProperty() { + return descrVisibility; } @Override @@ -414,9 +391,10 @@ public final class EventDetailChart extends XYChart impl return EventStripe.merge(u, v); } ); - EventStripeNode clusterNode = new EventStripeNode(EventDetailChart.this, eventStripe, null); - stripeNodeMap.put(eventStripe, clusterNode); - nodeGroup.getChildren().add(clusterNode); + EventStripeNode stripeNode = new EventStripeNode(EventDetailChart.this, eventStripe, null); + stripeNodeMap.put(eventStripe, stripeNode); + nodeGroup.getChildren().add(stripeNode); + data.setNode(stripeNode); } @Override @@ -429,13 +407,24 @@ public final class EventDetailChart extends XYChart impl protected synchronized void dataItemRemoved(Data data, Series series) { EventCluster eventCluster = data.getYValue(); - EventStripe removedCluster = stripeDescMap.remove(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription())); - EventStripeNode removedNode = stripeNodeMap.remove(removedCluster); + EventStripe removedStripe = stripeDescMap.remove(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription())); + EventStripeNode removedNode = stripeNodeMap.remove(removedStripe); nodeGroup.getChildren().remove(removedNode); - data.setNode(null); } + synchronized void setRequiresLayout(boolean b) { + requiresLayout = true; + } + + /** + * make this accessible to {@link EventStripeNode} + */ + @Override + protected void requestChartLayout() { + super.requestChartLayout(); + } + @Override protected void layoutChildren() { super.layoutChildren(); @@ -507,11 +496,7 @@ public final class EventDetailChart extends XYChart impl requiresLayout = true; } - synchronized SimpleObjectProperty< DescriptionVisibility> getDescrVisibility() { - return descrVisibility; - } - - synchronized ReadOnlyDoubleProperty getMaxVScroll() { + ReadOnlyDoubleProperty maxVScrollProperty() { return maxY.getReadOnlyProperty(); } @@ -523,7 +508,7 @@ public final class EventDetailChart extends XYChart impl .filter(p).collect(Collectors.toList()); } - public static Stream flatten(EventStripeNode node) { + private static Stream flatten(EventStripeNode node) { return Stream.concat( Stream.of(node), node.getSubNodes().stream().flatMap(EventDetailChart::flatten)); @@ -533,14 +518,7 @@ public final class EventDetailChart extends XYChart impl return getNodes(x -> true); } - synchronized SimpleDoubleProperty - getTruncateWidth() { - return truncateWidth; - } - - synchronized void - setVScroll(double d - ) { + synchronized void setVScroll(double d) { final double h = maxY.get() - (getHeight() * .9); nodeGroup.setTranslateY(-d * h); } @@ -666,41 +644,26 @@ public final class EventDetailChart extends XYChart impl return localMax - minY; } - private static final int DEFAULT_ROW_HEIGHT = 24; - private void layoutProjectionMap() { for (final Map.Entry, Line> entry : projectionMap.entrySet()) { - final Range eventBundle = entry.getKey(); + final Range range = entry.getKey(); final Line line = entry.getValue(); - line.setStartX(getParentXForValue(new DateTime(eventBundle.lowerEndpoint(), TimeLineController.getJodaTimeZone()))); - line.setEndX(getParentXForValue(new DateTime(eventBundle.upperEndpoint(), TimeLineController.getJodaTimeZone()))); + line.setStartX(getParentXForEpochMillis(range.lowerEndpoint())); + line.setEndX(getParentXForEpochMillis(range.upperEndpoint())); line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); } } - private double getParentXForValue(DateTime dt) { - return getXAxis().localToParent(getXAxis().getDisplayPosition(dt), 0).getX(); + private double getParentXForEpochMillis(Long epochMillis) { + DateTime dateTime = new DateTime(epochMillis, TimeLineController.getJodaTimeZone()); + return getXAxis().localToParent(getXAxis().getDisplayPosition(dateTime), 0).getX(); } - /** - * @return the controller - */ - public TimeLineController getController() { - return controller; - } + static private class DetailIntervalSelector extends IntervalSelector { - /** - * @return the filteredEvents - */ - public FilteredEventsModel getFilteredEvents() { - return filteredEvents; - } - - private class DetailIntervalSelector extends IntervalSelector { - - public DetailIntervalSelector(double x, double height, Axis axis, TimeLineController controller) { + DetailIntervalSelector(double x, double height, Axis axis, TimeLineController controller) { super(x, height, axis, controller); } @@ -718,18 +681,30 @@ public final class EventDetailChart extends XYChart impl protected DateTime parseDateTime(DateTime date) { return date; } - } - synchronized void setRequiresLayout(boolean b) { - requiresLayout = true; - } + private class PlaceMarkerAction extends Action { - /** - * make this accessible to AggregateEventNode - */ - @Override - protected void requestChartLayout() { - super.requestChartLayout(); + PlaceMarkerAction(MouseEvent clickEvent) { + super(EventDetailChart_chartContextMenu_placeMarker_name()); + + setGraphic(new ImageView(MARKER)); // NON-NLS + setEventHandler(actionEvent -> { + if (guideLine == null) { + guideLine = new GuideLine(0, 0, 0, getHeight(), getXAxis()); + guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); + guideLine.endYProperty().bind(heightProperty().subtract(getXAxis().heightProperty().subtract(getXAxis().tickLengthProperty()))); + getChartChildren().add(guideLine); + guideLine.setOnMouseClicked(mouseEvent -> { + if (mouseEvent.getButton() == MouseButton.SECONDARY) { + clearGuideLine(); + mouseEvent.consume(); + } + }); + } else { + guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); + } + }); + } } }