mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-15 09:17:42 +00:00
Merge pull request #1596 from millmanorama/cleanup_new_clustering
Cleanup new clustering, after https://github.com/sleuthkit/autopsy/pull/1595
This commit is contained in:
commit
00a6c19157
@ -1,554 +0,0 @@
|
|||||||
/*
|
|
||||||
* Autopsy Forensic Browser
|
|
||||||
*
|
|
||||||
* Copyright 2015 Basis Technology Corp.
|
|
||||||
* Contact: carrier <at> sleuthkit <dot> org
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
|
||||||
|
|
||||||
import 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<T, S>> extends StackPane implements DetailViewNode<AbstractDetailViewNode<T, S>> {
|
|
||||||
|
|
||||||
static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
|
|
||||||
static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
|
|
||||||
static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
|
|
||||||
static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
|
|
||||||
static final CornerRadii CORNER_RADII = new CornerRadii(3);
|
|
||||||
/**
|
|
||||||
* 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<EventType, DropShadow> 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<DescriptionLOD> 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<S> getSubNodes() {
|
|
||||||
return subNodePane.getChildrenUnmodifiable().stream()
|
|
||||||
.map(t -> (S) t)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* apply the 'effect' to visually indicate selection
|
|
||||||
*
|
|
||||||
* @param applied true to apply the selection 'effect', false to remove it
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void applySelectionEffect(boolean applied) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
if (applied) {
|
|
||||||
setBorder(selectionBorder);
|
|
||||||
} else {
|
|
||||||
setBorder(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @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<T> makeBundlesFromClusters(List<EventCluster> 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<List<S>> loggedTask;
|
|
||||||
loggedTask = new LoggedTask<List<S>>(
|
|
||||||
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
|
|
||||||
private Collection<T> bundles;
|
|
||||||
private volatile DescriptionLOD loadedDescriptionLoD = getDescLOD().withRelativeDetail(relativeDetail);
|
|
||||||
private DescriptionLOD next = loadedDescriptionLoD;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<S> 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<T> loadBundles() {
|
|
||||||
return makeBundlesFromClusters(eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void succeeded() {
|
|
||||||
chart.setCursor(Cursor.WAIT);
|
|
||||||
try {
|
|
||||||
List<S> 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<MouseEvent> {
|
|
||||||
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,8 @@
|
|||||||
Timeline.ui.detailview.tooltip.text={0}\nRight-click to remove.\nRight-drag to reposition.
|
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.name=Update Details
|
||||||
DetailViewPane.loggedTask.preparing=preparing
|
DetailViewPane.loggedTask.preparing=preparing
|
||||||
DetailViewPane.loggedTask.queryDb=querying db
|
DetailViewPane.loggedTask.queryDb=querying db
|
||||||
DetailViewPane.loggedTask.updateUI=updating ui
|
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.truncateSliderLabel.text=max description width (px)\:
|
||||||
DetailViewPane.advancedLayoutOptionsButtonLabel.text=Advanced Layout Options
|
DetailViewPane.advancedLayoutOptionsButtonLabel.text=Advanced Layout Options
|
||||||
DetailViewPane.bandByTypeBox.text=Band by Type
|
DetailViewPane.bandByTypeBox.text=Band by Type
|
||||||
|
@ -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<S extends DetailViewNode<S>> {
|
|
||||||
|
|
||||||
public void setDescriptionVisibility(DescriptionVisibility get);
|
|
||||||
|
|
||||||
public List<? extends S> getSubNodes();
|
|
||||||
|
|
||||||
public void setSpanWidths(List<Double> 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<Long> getEventIDs() {
|
|
||||||
return getEventBundle().getEventIDs();
|
|
||||||
}
|
|
||||||
|
|
||||||
default public long getStartMillis() {
|
|
||||||
return getEventBundle().getStartMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
default long getEndMillis() {
|
|
||||||
return getEventBundle().getEndMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static class StartTimeComparator implements Comparator<DetailViewNode<?>> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compare(DetailViewNode<?> o1, DetailViewNode<?> o2) {
|
|
||||||
return Long.compare(o1.getStartMillis(), o2.getStartMillis());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -99,7 +99,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
|
|||||||
* TODO: refactor common code out of this class and CountsChartPane into
|
* TODO: refactor common code out of this class and CountsChartPane into
|
||||||
* {@link AbstractVisualization}
|
* {@link AbstractVisualization}
|
||||||
*/
|
*/
|
||||||
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, DetailViewNode<?>, EventDetailChart> {
|
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, EventStripeNode, EventDetailChart> {
|
||||||
|
|
||||||
private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName());
|
private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName());
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
|
|
||||||
private final ObservableList<EventCluster> aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
private final ObservableList<EventCluster> aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
||||||
|
|
||||||
private final ObservableList<DetailViewNode<?>> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
private final ObservableList<EventStripeNode> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
||||||
|
|
||||||
public ObservableList<EventCluster> getAggregatedEvents() {
|
public ObservableList<EventCluster> getAggregatedEvents() {
|
||||||
return aggregatedEvents;
|
return aggregatedEvents;
|
||||||
@ -145,16 +145,16 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
|
|
||||||
dateAxis.setAutoRanging(false);
|
dateAxis.setAutoRanging(false);
|
||||||
region.minHeightProperty().bind(dateAxis.heightProperty());
|
region.minHeightProperty().bind(dateAxis.heightProperty());
|
||||||
vertScrollBar.visibleAmountProperty().bind(chart.heightProperty().multiply(100).divide(chart.getMaxVScroll()));
|
vertScrollBar.visibleAmountProperty().bind(chart.heightProperty().multiply(100).divide(chart.maxVScrollProperty()));
|
||||||
requestLayout();
|
requestLayout();
|
||||||
|
|
||||||
highlightedNodes.addListener((ListChangeListener.Change<? extends DetailViewNode<?>> change) -> {
|
highlightedNodes.addListener((ListChangeListener.Change<? extends EventStripeNode> change) -> {
|
||||||
while (change.next()) {
|
while (change.next()) {
|
||||||
change.getAddedSubList().forEach(aeNode -> {
|
change.getAddedSubList().forEach(node -> {
|
||||||
aeNode.applyHighlightEffect(true);
|
node.applyHighlightEffect(true);
|
||||||
});
|
});
|
||||||
change.getRemoved().forEach(aeNode -> {
|
change.getRemoved().forEach(node -> {
|
||||||
aeNode.applyHighlightEffect(false);
|
node.applyHighlightEffect(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -211,8 +211,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
selectedNodes.addListener((Observable observable) -> {
|
selectedNodes.addListener((Observable observable) -> {
|
||||||
highlightedNodes.clear();
|
highlightedNodes.clear();
|
||||||
selectedNodes.stream().forEach((tn) -> {
|
selectedNodes.stream().forEach((tn) -> {
|
||||||
for (DetailViewNode<?> n : chart.getNodes((DetailViewNode<?> t) ->
|
for (EventStripeNode n : chart.getNodes((EventStripeNode t) ->
|
||||||
t.getDescription().equals(tn.getDescription()))) {
|
t.getDescription().equals(tn.getDescription()))) {
|
||||||
highlightedNodes.add(n);
|
highlightedNodes.add(n);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -226,7 +226,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void incrementScrollValue(int factor) {
|
private void incrementScrollValue(int factor) {
|
||||||
vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() + factor * (chart.getHeight() / chart.getMaxVScroll().get()))));
|
vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() + factor * (chart.getHeight() / chart.maxVScrollProperty().get()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelectionModel(MultipleSelectionModel<TreeItem<NavTreeNode>> selectionModel) {
|
public void setSelectionModel(MultipleSelectionModel<TreeItem<NavTreeNode>> selectionModel) {
|
||||||
@ -235,8 +235,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
treeSelectionModel.getSelectedItems().addListener((Observable observable) -> {
|
treeSelectionModel.getSelectedItems().addListener((Observable observable) -> {
|
||||||
highlightedNodes.clear();
|
highlightedNodes.clear();
|
||||||
for (TreeItem<NavTreeNode> tn : treeSelectionModel.getSelectedItems()) {
|
for (TreeItem<NavTreeNode> tn : treeSelectionModel.getSelectedItems()) {
|
||||||
for (DetailViewNode<?> n : chart.getNodes((DetailViewNode<?> t) ->
|
for (EventStripeNode n : chart.getNodes((EventStripeNode t) ->
|
||||||
t.getDescription().equals(tn.getValue().getDescription()))) {
|
t.getDescription().equals(tn.getValue().getDescription()))) {
|
||||||
highlightedNodes.add(n);
|
highlightedNodes.add(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -356,8 +356,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void applySelectionEffect(DetailViewNode<?> c1, Boolean selected) {
|
protected void applySelectionEffect(EventStripeNode c1, Boolean selected) {
|
||||||
chart.applySelectionEffect(c1, selected);
|
c1.applySelectionEffect(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DetailViewSettingsPane extends HBox {
|
private class DetailViewSettingsPane extends HBox {
|
||||||
@ -422,7 +422,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
@FXML
|
@FXML
|
||||||
private SeparatorMenuItem descVisibilitySeparatorMenuItem;
|
private SeparatorMenuItem descVisibilitySeparatorMenuItem;
|
||||||
|
|
||||||
public DetailViewSettingsPane() {
|
DetailViewSettingsPane() {
|
||||||
FXMLConstructor.construct(this, "DetailViewSettingsPane.fxml"); // NON-NLS
|
FXMLConstructor.construct(this, "DetailViewSettingsPane.fxml"); // NON-NLS
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -448,11 +448,11 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
|
|
||||||
descrVisibility.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> {
|
descrVisibility.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> {
|
||||||
if (newToggle == countsRadio) {
|
if (newToggle == countsRadio) {
|
||||||
chart.getDescrVisibility().set(DescriptionVisibility.COUNT_ONLY);
|
chart.descrVisibilityProperty().set(DescriptionVisibility.COUNT_ONLY);
|
||||||
} else if (newToggle == showRadio) {
|
} else if (newToggle == showRadio) {
|
||||||
chart.getDescrVisibility().set(DescriptionVisibility.SHOWN);
|
chart.descrVisibilityProperty().set(DescriptionVisibility.SHOWN);
|
||||||
} else if (newToggle == hiddenRadio) {
|
} else if (newToggle == hiddenRadio) {
|
||||||
chart.getDescrVisibility().set(DescriptionVisibility.HIDDEN);
|
chart.descrVisibilityProperty().set(DescriptionVisibility.HIDDEN);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -478,7 +478,5 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
hiddenRadioMenuItem.setText(NbBundle.getMessage(this.getClass(), "DetailViewPane.hiddenRadioMenuItem.text"));
|
hiddenRadioMenuItem.setText(NbBundle.getMessage(this.getClass(), "DetailViewPane.hiddenRadioMenuItem.text"));
|
||||||
hiddenRadio.setText(NbBundle.getMessage(this.getClass(), "DetailViewPane.hiddenRadio.text"));
|
hiddenRadio.setText(NbBundle.getMessage(this.getClass(), "DetailViewPane.hiddenRadio.text"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,181 +0,0 @@
|
|||||||
/*
|
|
||||||
* Autopsy Forensic Browser
|
|
||||||
*
|
|
||||||
* Copyright 2013-15 Basis Technology Corp.
|
|
||||||
* Contact: carrier <at> sleuthkit <dot> org
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
|
||||||
|
|
||||||
import 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<EventCluster, EventClusterNode> {
|
|
||||||
|
|
||||||
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> 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<String, Long> hashSetCounts = new HashMap<>();
|
|
||||||
if (!getEventCluster().getEventIDsWithHashHits().isEmpty()) {
|
|
||||||
hashSetCounts = new HashMap<>();
|
|
||||||
try {
|
|
||||||
for (TimeLineEvent tle : getEventsModel().getEventsById(getEventCluster().getEventIDsWithHashHits())) {
|
|
||||||
Set<String> 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<String, Long> tagCounts = new HashMap<>();
|
|
||||||
if (!getEventCluster().getEventIDsWithTags().isEmpty()) {
|
|
||||||
tagCounts.putAll(getEventsModel().getTagCountsByTagName(getEventCluster().getEventIDsWithTags()));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
String hashSetCountsString = hashSetCounts.entrySet().stream()
|
|
||||||
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
|
|
||||||
.collect(Collectors.joining("\n"));
|
|
||||||
String tagCountsString = tagCounts.entrySet().stream()
|
|
||||||
.map((Map.Entry<String, Long> 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<Double> 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<EventCluster> makeBundlesFromClusters(List<EventCluster> eventClusters) {
|
|
||||||
return eventClusters;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
EventClusterNode getNodeForBundle(EventCluster cluster) {
|
|
||||||
return new EventClusterNode(cluster, this, getChart());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -29,7 +29,6 @@ import java.util.Iterator;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.MissingResourceException;
|
import java.util.MissingResourceException;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
@ -38,7 +37,6 @@ import javafx.animation.KeyValue;
|
|||||||
import javafx.animation.Timeline;
|
import javafx.animation.Timeline;
|
||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.property.Property;
|
|
||||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
@ -47,12 +45,10 @@ import javafx.beans.property.SimpleObjectProperty;
|
|||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Group;
|
import javafx.scene.Group;
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.chart.Axis;
|
import javafx.scene.chart.Axis;
|
||||||
import javafx.scene.chart.NumberAxis;
|
import javafx.scene.chart.NumberAxis;
|
||||||
import javafx.scene.chart.XYChart;
|
import javafx.scene.chart.XYChart;
|
||||||
@ -80,6 +76,8 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
|
|||||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
|
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
|
* Custom implementation of {@link XYChart} to graph events on a horizontal
|
||||||
@ -97,15 +95,10 @@ import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
|
|||||||
*/
|
*/
|
||||||
public final class EventDetailChart extends XYChart<DateTime, EventCluster> implements TimeLineChart<DateTime> {
|
public final class EventDetailChart extends XYChart<DateTime, EventCluster> implements TimeLineChart<DateTime> {
|
||||||
|
|
||||||
|
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_Y_OFFSET = 5;
|
||||||
|
|
||||||
private static final int PROJECTED_LINE_STROKE_WIDTH = 5;
|
private static final int PROJECTED_LINE_STROKE_WIDTH = 5;
|
||||||
|
private static final int DEFAULT_ROW_HEIGHT = 24;
|
||||||
/**
|
|
||||||
* 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 ContextMenu chartContextMenu;
|
private ContextMenu chartContextMenu;
|
||||||
|
|
||||||
@ -113,11 +106,6 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
|
|
||||||
private FilteredEventsModel filteredEvents;
|
private FilteredEventsModel filteredEvents;
|
||||||
|
|
||||||
/**
|
|
||||||
* how much detail of the description to show in the ui
|
|
||||||
*/
|
|
||||||
private final SimpleObjectProperty<DescriptionVisibility> descrVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a user position-able vertical line to help the compare events
|
* a user position-able vertical line to help the compare events
|
||||||
*/
|
*/
|
||||||
@ -133,47 +121,32 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
/**
|
/**
|
||||||
* listener that triggers layout pass
|
* listener that triggers layout pass
|
||||||
*/
|
*/
|
||||||
private final InvalidationListener layoutInvalidationListener = (
|
private final InvalidationListener layoutInvalidationListener = (Observable o) -> {
|
||||||
Observable o) -> {
|
synchronized (EventDetailChart.this) {
|
||||||
synchronized (EventDetailChart.this) {
|
requiresLayout = true;
|
||||||
requiresLayout = true;
|
requestChartLayout();
|
||||||
requestChartLayout();
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the maximum y value used so far during the most recent layout pass
|
* the maximum y value used so far during the most recent layout pass
|
||||||
*/
|
*/
|
||||||
private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0);
|
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();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* map from event to node
|
|
||||||
*/
|
|
||||||
private final Map<EventCluster, EventClusterNode> clusterNodeMap = new HashMap<>();
|
|
||||||
private final Map<ImmutablePair<EventType, String>, EventStripe> stripeDescMap = new HashMap<>();
|
|
||||||
private final Map<EventStripe, EventStripeNode> 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<Range<Long>, Line> projectionMap = new HashMap<>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* flag indicating whether this chart actually needs a layout pass
|
* flag indicating whether this chart actually needs a layout pass
|
||||||
*/
|
*/
|
||||||
@GuardedBy(value = "this")
|
@GuardedBy(value = "this")
|
||||||
private boolean requiresLayout = true;
|
private boolean requiresLayout = true;
|
||||||
|
|
||||||
final ObservableList<DetailViewNode<?>> selectedNodes;
|
final ObservableList<EventStripeNode> 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<ImmutablePair<EventType, String>, EventStripe> stripeDescMap = new HashMap<>();
|
||||||
|
private final Map<EventStripe, EventStripeNode> stripeNodeMap = new HashMap<>();
|
||||||
|
private final Map<Range<Long>, Line> projectionMap = new HashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* list of series of data added to this chart TODO: replace this with a map
|
* list of series of data added to this chart TODO: replace this with a map
|
||||||
@ -187,6 +160,23 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
final List<String> collect = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList());
|
final List<String> collect = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList());
|
||||||
return Integer.compare(collect.indexOf(s1.getName()), collect.indexOf(s2.getName()));
|
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<DescriptionVisibility> descrVisibility
|
||||||
|
= new SimpleObjectProperty<>(DescriptionVisibility.SHOWN);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* true == truncate all the labels to the greater of the size of their
|
* true == truncate all the labels to the greater of the size of their
|
||||||
@ -201,13 +191,12 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
* via slider if truncateAll is true
|
* via slider if truncateAll is true
|
||||||
*/
|
*/
|
||||||
private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
|
private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
|
||||||
private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true);
|
|
||||||
|
|
||||||
EventDetailChart(DateAxis dateAxis, final Axis<EventCluster> verticalAxis, ObservableList<DetailViewNode<?>> selectedNodes) {
|
EventDetailChart(DateAxis dateAxis, final Axis<EventCluster> verticalAxis, ObservableList<EventStripeNode> selectedNodes) {
|
||||||
super(dateAxis, verticalAxis);
|
super(dateAxis, verticalAxis);
|
||||||
dateAxis.setAutoRanging(false);
|
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.setTickLabelsVisible(false);
|
||||||
verticalAxis.setTickMarkVisible(false);
|
verticalAxis.setTickMarkVisible(false);
|
||||||
|
|
||||||
@ -218,17 +207,17 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
//all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly
|
//all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly
|
||||||
getPlotChildren().add(nodeGroup);
|
getPlotChildren().add(nodeGroup);
|
||||||
|
|
||||||
//bind listener to events that should trigger layout
|
//add listener for events that should trigger layout
|
||||||
widthProperty().addListener(layoutInvalidationListener);
|
widthProperty().addListener(layoutInvalidationListener);
|
||||||
heightProperty().addListener(layoutInvalidationListener);
|
heightProperty().addListener(layoutInvalidationListener);
|
||||||
|
|
||||||
bandByType.addListener(layoutInvalidationListener);
|
bandByType.addListener(layoutInvalidationListener);
|
||||||
oneEventPerRow.addListener(layoutInvalidationListener);
|
oneEventPerRow.addListener(layoutInvalidationListener);
|
||||||
truncateAll.addListener(layoutInvalidationListener);
|
truncateAll.addListener(layoutInvalidationListener);
|
||||||
truncateWidth.addListener(layoutInvalidationListener);
|
truncateWidth.addListener(layoutInvalidationListener);
|
||||||
descrVisibility.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) -> {
|
boundsInLocalProperty().addListener((Observable observable) -> {
|
||||||
setPrefHeight(boundsInLocalProperty().get().getHeight());
|
setPrefHeight(boundsInLocalProperty().get().getHeight());
|
||||||
});
|
});
|
||||||
@ -255,18 +244,18 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
|
|
||||||
this.selectedNodes = selectedNodes;
|
this.selectedNodes = selectedNodes;
|
||||||
this.selectedNodes.addListener((
|
this.selectedNodes.addListener((
|
||||||
ListChangeListener.Change<? extends DetailViewNode<?>> c) -> {
|
ListChangeListener.Change<? extends EventStripeNode> c) -> {
|
||||||
while (c.next()) {
|
while (c.next()) {
|
||||||
c.getRemoved().forEach((DetailViewNode<?> t) -> {
|
c.getRemoved().forEach((EventStripeNode t) -> {
|
||||||
t.getEventBundle().getRanges().forEach((Range<Long> t1) -> {
|
t.getEventStripe().getRanges().forEach((Range<Long> t1) -> {
|
||||||
Line removedLine = projectionMap.remove(t1);
|
Line removedLine = projectionMap.remove(t1);
|
||||||
getChartChildren().removeAll(removedLine);
|
getChartChildren().removeAll(removedLine);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
c.getAddedSubList().forEach((DetailViewNode<?> t) -> {
|
c.getAddedSubList().forEach((EventStripeNode t) -> {
|
||||||
|
|
||||||
for (Range<Long> range : t.getEventBundle().getRanges()) {
|
for (Range<Long> 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,
|
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
|
dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.upperEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
|
||||||
@ -281,44 +270,25 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.controller.selectEventIDs(selectedNodes.stream()
|
this.controller.selectEventIDs(selectedNodes.stream()
|
||||||
.flatMap(detailNode -> detailNode.getEventIDs().stream())
|
.flatMap(detailNode -> detailNode.getEventsIDs().stream())
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
});
|
});
|
||||||
|
|
||||||
requestChartLayout();
|
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 {
|
ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException {
|
||||||
if (chartContextMenu != null) {
|
if (chartContextMenu != null) {
|
||||||
chartContextMenu.hide();
|
chartContextMenu.hide();
|
||||||
}
|
}
|
||||||
chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new Action(
|
chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(clickEvent),
|
||||||
NbBundle.getMessage(this.getClass(), "EventDetailChart.chartContextMenu.placeMarker.name")) {
|
new ActionGroup(EventDetailChart_contextMenu_zoomHistory_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"),
|
|
||||||
new Back(controller),
|
new Back(controller),
|
||||||
new Forward(controller))));
|
new Forward(controller))));
|
||||||
chartContextMenu.setAutoHide(true);
|
chartContextMenu.setAutoHide(true);
|
||||||
@ -391,49 +361,40 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
getChartChildren().add(getIntervalSelector());
|
getChartChildren().add(getIntervalSelector());
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized SimpleBooleanProperty oneEventPerRowProperty() {
|
SimpleBooleanProperty oneEventPerRowProperty() {
|
||||||
return oneEventPerRow;
|
return oneEventPerRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized SimpleBooleanProperty truncateAllProperty() {
|
SimpleDoubleProperty getTruncateWidth() {
|
||||||
|
return truncateWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleBooleanProperty truncateAllProperty() {
|
||||||
return truncateAll;
|
return truncateAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void setEventOnePerRow(Boolean t1) {
|
SimpleObjectProperty< DescriptionVisibility> descrVisibilityProperty() {
|
||||||
oneEventPerRow.set(t1);
|
return descrVisibility;
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void setTruncateAll(Boolean t1) {
|
|
||||||
truncateAll.set(t1);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected synchronized void dataItemAdded(Series<DateTime, EventCluster> series, int i, Data<DateTime, EventCluster> data) {
|
protected synchronized void dataItemAdded(Series<DateTime, EventCluster> series, int i, Data<DateTime, EventCluster> data) {
|
||||||
final EventCluster aggEvent = data.getYValue();
|
final EventCluster eventCluster = data.getYValue();
|
||||||
if (alternateLayout.get()) {
|
|
||||||
EventStripe eventCluster = stripeDescMap.merge(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription()),
|
EventStripe eventStripe = stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()),
|
||||||
new EventStripe(aggEvent),
|
new EventStripe(eventCluster),
|
||||||
(EventStripe u, EventStripe v) -> {
|
(EventStripe u, EventStripe v) -> {
|
||||||
EventStripeNode remove = stripeNodeMap.remove(u);
|
EventStripeNode remove = stripeNodeMap.remove(u);
|
||||||
nodeGroup.getChildren().remove(remove);
|
nodeGroup.getChildren().remove(remove);
|
||||||
remove = stripeNodeMap.remove(v);
|
remove = stripeNodeMap.remove(v);
|
||||||
nodeGroup.getChildren().remove(remove);
|
nodeGroup.getChildren().remove(remove);
|
||||||
return EventStripe.merge(u, v);
|
return EventStripe.merge(u, v);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
EventStripeNode clusterNode = new EventStripeNode(eventCluster, null, EventDetailChart.this);
|
EventStripeNode stripeNode = new EventStripeNode(EventDetailChart.this, eventStripe, null);
|
||||||
stripeNodeMap.put(eventCluster, clusterNode);
|
stripeNodeMap.put(eventStripe, stripeNode);
|
||||||
nodeGroup.getChildren().add(clusterNode);
|
nodeGroup.getChildren().add(stripeNode);
|
||||||
} else {
|
data.setNode(stripeNode);
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -444,17 +405,26 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected synchronized void dataItemRemoved(Data<DateTime, EventCluster> data, Series<DateTime, EventCluster> series) {
|
protected synchronized void dataItemRemoved(Data<DateTime, EventCluster> data, Series<DateTime, EventCluster> series) {
|
||||||
EventCluster aggEvent = data.getYValue();
|
EventCluster eventCluster = data.getYValue();
|
||||||
Node removedNode = clusterNodeMap.remove(aggEvent);
|
|
||||||
nodeGroup.getChildren().remove(removedNode);
|
|
||||||
|
|
||||||
EventStripe removedCluster = stripeDescMap.remove(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription()));
|
EventStripe removedStripe = stripeDescMap.remove(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()));
|
||||||
removedNode = stripeNodeMap.remove(removedCluster);
|
EventStripeNode removedNode = stripeNodeMap.remove(removedStripe);
|
||||||
nodeGroup.getChildren().remove(removedNode);
|
nodeGroup.getChildren().remove(removedNode);
|
||||||
|
|
||||||
data.setNode(null);
|
data.setNode(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void setRequiresLayout(boolean b) {
|
||||||
|
requiresLayout = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* make this accessible to {@link EventStripeNode}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void requestChartLayout() {
|
||||||
|
super.requestChartLayout();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void layoutChildren() {
|
protected void layoutChildren() {
|
||||||
super.layoutChildren();
|
super.layoutChildren();
|
||||||
@ -486,36 +456,19 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
maxY.set(0.0);
|
maxY.set(0.0);
|
||||||
|
|
||||||
if (bandByType.get() == false) {
|
if (bandByType.get() == false) {
|
||||||
if (alternateLayout.get() == true) {
|
List<EventStripeNode> nodes = new ArrayList<>(stripeNodeMap.values());
|
||||||
List<EventStripeNode> nodes = new ArrayList<>(stripeNodeMap.values());
|
nodes.sort(Comparator.comparing(EventStripeNode::getStartMillis));
|
||||||
nodes.sort(Comparator.comparing(DetailViewNode<?>::getStartMillis));
|
layoutNodes(nodes, minY, 0);
|
||||||
layoutNodes(nodes, minY, 0);
|
|
||||||
} else {
|
|
||||||
List<EventClusterNode> nodes = new ArrayList<>(clusterNodeMap.values());
|
|
||||||
nodes.sort(Comparator.comparing(DetailViewNode<?>::getStartMillis));
|
|
||||||
layoutNodes(nodes, minY, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
for (Series<DateTime, EventCluster> s : sortedSeriesList) {
|
for (Series<DateTime, EventCluster> s : sortedSeriesList) {
|
||||||
if (alternateLayout.get() == true) {
|
List<EventStripeNode> nodes = s.getData().stream()
|
||||||
List<EventStripeNode> nodes = s.getData().stream()
|
.map(Data::getYValue)
|
||||||
.map(Data::getYValue)
|
.map(cluster -> stripeDescMap.get(ImmutablePair.of(cluster.getEventType(), cluster.getDescription())))
|
||||||
.map(cluster -> stripeDescMap.get(ImmutablePair.of(cluster.getEventType(), cluster.getDescription())))
|
.distinct()
|
||||||
.distinct()
|
.sorted(Comparator.comparing(EventStripe::getStartMillis))
|
||||||
.sorted(Comparator.comparing(EventStripe::getStartMillis))
|
.map(stripeNodeMap::get)
|
||||||
.map(stripeNodeMap::get)
|
.collect(Collectors.toList());
|
||||||
.collect(Collectors.toList());
|
layoutNodes(nodes, minY, 0);
|
||||||
layoutNodes(nodes, minY, 0);
|
|
||||||
} else {
|
|
||||||
List<EventClusterNode> 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);
|
|
||||||
}
|
|
||||||
minY = maxY.get();
|
minY = maxY.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -543,43 +496,29 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
requiresLayout = true;
|
requiresLayout = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized SimpleObjectProperty< DescriptionVisibility> getDescrVisibility() {
|
ReadOnlyDoubleProperty maxVScrollProperty() {
|
||||||
return descrVisibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized ReadOnlyDoubleProperty getMaxVScroll() {
|
|
||||||
return maxY.getReadOnlyProperty();
|
return maxY.getReadOnlyProperty();
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterable<DetailViewNode<?>> getNodes(Predicate<DetailViewNode<?>> p) {
|
Iterable<EventStripeNode> getNodes(Predicate<EventStripeNode> p) {
|
||||||
Collection<? extends DetailViewNode<?>> values = alternateLayout.get()
|
Collection<EventStripeNode> values = stripeNodeMap.values();
|
||||||
? stripeNodeMap.values()
|
|
||||||
: clusterNodeMap.values();
|
|
||||||
|
|
||||||
//collapse tree of DetailViewNoeds to list and then filter on given predicate
|
//collapse tree of DetailViewNoeds to list and then filter on given predicate
|
||||||
return values.stream()
|
return values.stream()
|
||||||
.flatMap(EventDetailChart::flatten)
|
.flatMap(EventDetailChart::flatten)
|
||||||
.filter(p).collect(Collectors.toList());
|
.filter(p).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Stream<? extends DetailViewNode<?>> flatten(DetailViewNode<?> node) {
|
private static Stream<EventStripeNode> flatten(EventStripeNode node) {
|
||||||
return Stream.concat(
|
return Stream.concat(
|
||||||
Stream.of(node),
|
Stream.of(node),
|
||||||
node.getSubNodes().stream().flatMap(EventDetailChart::flatten));
|
node.getSubNodes().stream().flatMap(EventDetailChart::flatten));
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterable<DetailViewNode<?>> getAllNodes() {
|
Iterable<EventStripeNode> getAllNodes() {
|
||||||
return getNodes(x -> true);
|
return getNodes(x -> true);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized SimpleDoubleProperty
|
synchronized void setVScroll(double d) {
|
||||||
getTruncateWidth() {
|
|
||||||
return truncateWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void
|
|
||||||
setVScroll(double d
|
|
||||||
) {
|
|
||||||
final double h = maxY.get() - (getHeight() * .9);
|
final double h = maxY.get() - (getHeight() * .9);
|
||||||
nodeGroup.setTranslateY(-d * h);
|
nodeGroup.setTranslateY(-d * h);
|
||||||
}
|
}
|
||||||
@ -596,12 +535,13 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
* @param nodes
|
* @param nodes
|
||||||
* @param minY
|
* @param minY
|
||||||
*/
|
*/
|
||||||
private synchronized double layoutNodes(final Collection<? extends AbstractDetailViewNode<?, ?>> 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
|
//hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot
|
||||||
Map<Integer, Double> maxXatY = new HashMap<>();
|
Map<Integer, Double> maxXatY = new HashMap<>();
|
||||||
double localMax = minY;
|
double localMax = minY;
|
||||||
//for each node lay size it and position it in first available slot
|
//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());
|
node.setDescriptionVisibility(descrVisibility.get());
|
||||||
double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis()));
|
double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis()));
|
||||||
|
|
||||||
@ -610,48 +550,40 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
double layoutNodesResultHeight = 0;
|
double layoutNodesResultHeight = 0;
|
||||||
|
|
||||||
double span = 0;
|
double span = 0;
|
||||||
List<? extends AbstractDetailViewNode<?, ?>> subNodes = node.getSubNodes();
|
List<EventStripeNode> subNodes = node.getSubNodes();
|
||||||
if (subNodes.isEmpty() == false) {
|
if (subNodes.isEmpty() == false) {
|
||||||
subNodes.sort(new DetailViewNode.StartTimeComparator());
|
subNodes.sort(Comparator.comparing(EventStripeNode::getStartMillis));
|
||||||
layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition);
|
layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alternateLayout.get() == false) {
|
List<Double> spanWidths = new ArrayList<>();
|
||||||
double endX = getXAxis().getDisplayPosition(new DateTime(node.getEndMillis())) - xOffset;
|
double x = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis()));;
|
||||||
span = endX - startX;
|
double x2;
|
||||||
//size timespan border
|
Iterator<Range<Long>> ranges = node.getStripe().getRanges().iterator();
|
||||||
node.setSpanWidths(Arrays.asList(span));
|
Range<Long> range = ranges.next();
|
||||||
} else {
|
do {
|
||||||
|
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
|
||||||
EventStripeNode stripeNode = (EventStripeNode) node;
|
double clusterSpan = x2 - x;
|
||||||
List<Double> spanWidths = new ArrayList<>();
|
span += clusterSpan;
|
||||||
double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));;
|
spanWidths.add(clusterSpan);
|
||||||
double x2;
|
if (ranges.hasNext()) {
|
||||||
Iterator<Range<Long>> ranges = stripeNode.getStripe().getRanges().iterator();
|
range = ranges.next();
|
||||||
Range<Long> range = ranges.next();
|
x = getXAxis().getDisplayPosition(new DateTime(range.lowerEndpoint()));
|
||||||
do {
|
double gapSpan = x - x2;
|
||||||
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
|
span += gapSpan;
|
||||||
double clusterSpan = x2 - x;
|
spanWidths.add(gapSpan);
|
||||||
span += clusterSpan;
|
if (ranges.hasNext() == false) {
|
||||||
spanWidths.add(clusterSpan);
|
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
|
||||||
if (ranges.hasNext()) {
|
clusterSpan = x2 - x;
|
||||||
range = ranges.next();
|
span += clusterSpan;
|
||||||
x = getXAxis().getDisplayPosition(new DateTime(range.lowerEndpoint()));
|
spanWidths.add(clusterSpan);
|
||||||
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
|
if (truncateAll.get()) { //if truncate option is selected limit width of description label
|
||||||
node.setDescriptionWidth(Math.max(span, truncateWidth.get()));
|
node.setDescriptionWidth(Math.max(span, truncateWidth.get()));
|
||||||
} else { //else set it unbounded
|
} else { //else set it unbounded
|
||||||
@ -712,45 +644,26 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
return localMax - minY;
|
return localMax - minY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int DEFAULT_ROW_HEIGHT = 24;
|
|
||||||
|
|
||||||
private void layoutProjectionMap() {
|
private void layoutProjectionMap() {
|
||||||
for (final Map.Entry<Range<Long>, Line> entry : projectionMap.entrySet()) {
|
for (final Map.Entry<Range<Long>, Line> entry : projectionMap.entrySet()) {
|
||||||
final Range<Long> eventBundle = entry.getKey();
|
final Range<Long> range = entry.getKey();
|
||||||
final Line line = entry.getValue();
|
final Line line = entry.getValue();
|
||||||
|
|
||||||
line.setStartX(getParentXForValue(new DateTime(eventBundle.lowerEndpoint(), TimeLineController.getJodaTimeZone())));
|
line.setStartX(getParentXForEpochMillis(range.lowerEndpoint()));
|
||||||
line.setEndX(getParentXForValue(new DateTime(eventBundle.upperEndpoint(), TimeLineController.getJodaTimeZone())));
|
line.setEndX(getParentXForEpochMillis(range.upperEndpoint()));
|
||||||
line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
|
line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
|
||||||
line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
|
line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private double getParentXForValue(DateTime dt) {
|
private double getParentXForEpochMillis(Long epochMillis) {
|
||||||
return getXAxis().localToParent(getXAxis().getDisplayPosition(dt), 0).getX();
|
DateTime dateTime = new DateTime(epochMillis, TimeLineController.getJodaTimeZone());
|
||||||
|
return getXAxis().localToParent(getXAxis().getDisplayPosition(dateTime), 0).getX();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static private class DetailIntervalSelector extends IntervalSelector<DateTime> {
|
||||||
* @return the controller
|
|
||||||
*/
|
|
||||||
public TimeLineController getController() {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
DetailIntervalSelector(double x, double height, Axis<DateTime> axis, TimeLineController controller) {
|
||||||
* @return the filteredEvents
|
|
||||||
*/
|
|
||||||
public FilteredEventsModel getFilteredEvents() {
|
|
||||||
return filteredEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
Property<Boolean> alternateLayoutProperty() {
|
|
||||||
return alternateLayout;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DetailIntervalSelector extends IntervalSelector<DateTime> {
|
|
||||||
|
|
||||||
public DetailIntervalSelector(double x, double height, Axis<DateTime> axis, TimeLineController controller) {
|
|
||||||
super(x, height, axis, controller);
|
super(x, height, axis, controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -768,22 +681,30 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
protected DateTime parseDateTime(DateTime date) {
|
protected DateTime parseDateTime(DateTime date) {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void setRequiresLayout(boolean b) {
|
private class PlaceMarkerAction extends Action {
|
||||||
requiresLayout = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
PlaceMarkerAction(MouseEvent clickEvent) {
|
||||||
* make this accessible to AggregateEventNode
|
super(EventDetailChart_chartContextMenu_placeMarker_name());
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void requestChartLayout() {
|
|
||||||
super.requestChartLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
void applySelectionEffect(DetailViewNode<?> c1, Boolean selected) {
|
setGraphic(new ImageView(MARKER)); // NON-NLS
|
||||||
c1.applySelectionEffect(selected);
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,66 +1,247 @@
|
|||||||
/*
|
/*
|
||||||
* To change this license header, choose License Headers in Project Properties.
|
* Autopsy Forensic Browser
|
||||||
* To change this template file, choose Tools | Templates
|
*
|
||||||
* and open the template in the editor.
|
* Copyright 2015 Basis Technology Corp.
|
||||||
|
* Contact: carrier <at> sleuthkit <dot> org
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||||
|
|
||||||
import com.google.common.collect.Range;
|
import com.google.common.collect.Range;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
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 java.util.stream.Collectors;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
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.Background;
|
||||||
import javafx.scene.layout.BackgroundFill;
|
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.HBox;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import static javafx.scene.layout.Region.USE_PREF_SIZE;
|
import static javafx.scene.layout.Region.USE_PREF_SIZE;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
|
import javafx.scene.paint.Color;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.controlsfx.control.action.Action;
|
||||||
|
import org.controlsfx.control.action.ActionUtils;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
|
import org.joda.time.Interval;
|
||||||
|
import org.openide.util.NbBundle;
|
||||||
|
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||||
|
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
|
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 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Node used in {@link EventDetailChart} to represent an EventStripe.
|
||||||
*/
|
*/
|
||||||
public class EventStripeNode extends AbstractDetailViewNode<EventStripe, EventStripeNode> {
|
final public class EventStripeNode extends StackPane {
|
||||||
|
|
||||||
private final HBox rangesHBox = new HBox();
|
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
|
||||||
|
|
||||||
EventStripeNode(EventStripe eventStripe, EventStripeNode parentNode, EventDetailChart chart) {
|
private static final CornerRadii CORNER_RADII_3 = new CornerRadii(3);
|
||||||
super(chart, eventStripe, parentNode);
|
private static final CornerRadii CORNER_RADII_1 = new CornerRadii(1);
|
||||||
minWidthProperty().bind(rangesHBox.widthProperty());
|
private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1);
|
||||||
final VBox internalVBox = new VBox(header, getSubNodePane());
|
private final static Map<EventType, DropShadow> dropShadowMap = new ConcurrentHashMap<>();
|
||||||
internalVBox.setAlignment(Pos.CENTER_LEFT);
|
private static final Border SELECTION_BORDER = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2)));
|
||||||
|
|
||||||
for (Range<Long> range : eventStripe.getRanges()) {
|
static void configureLoDButton(Button b) {
|
||||||
Region rangeRegion = new Region();
|
b.setMinSize(16, 16);
|
||||||
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
|
b.setMaxSize(16, 16);
|
||||||
rangeRegion.setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
|
b.setPrefSize(16, 16);
|
||||||
rangesHBox.getChildren().addAll(rangeRegion, new Region());
|
show(b, false);
|
||||||
}
|
}
|
||||||
rangesHBox.getChildren().remove(rangesHBox.getChildren().size() - 1);
|
|
||||||
rangesHBox.setMaxWidth(USE_PREF_SIZE);
|
static void show(Node b, boolean show) {
|
||||||
|
b.setVisible(show);
|
||||||
|
b.setManaged(show);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
|
||||||
|
private DescriptionVisibility descrVis;
|
||||||
|
private Tooltip tooltip;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
private final HBox clustersHBox = new HBox();
|
||||||
|
private final ImageView eventTypeImageView = new ImageView();
|
||||||
|
private final Label descrLabel = new Label("", eventTypeImageView);
|
||||||
|
private final Label countLabel = new Label();
|
||||||
|
private final Button plusButton = ActionUtils.createButton(new ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE);
|
||||||
|
private final Button minusButton = ActionUtils.createButton(new CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE);
|
||||||
|
private final ImageView hashIV = new ImageView(HASH_PIN);
|
||||||
|
private final ImageView tagIV = new ImageView(TAG);
|
||||||
|
private final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV, minusButton, plusButton);
|
||||||
|
|
||||||
|
private final 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;
|
||||||
|
|
||||||
|
public EventStripeNode(EventDetailChart chart, EventStripe eventStripe, EventStripeNode parentEventNode) {
|
||||||
|
this.eventStripe = eventStripe;
|
||||||
|
this.parentNode = parentEventNode;
|
||||||
|
this.chart = chart;
|
||||||
|
descLOD.set(eventStripe.getDescriptionLOD());
|
||||||
|
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
|
||||||
|
eventsModel = chart.getController().getEventsModel();
|
||||||
|
final Color evtColor = getEventType().getColor();
|
||||||
|
defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY));
|
||||||
|
highlightedBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY));
|
||||||
|
|
||||||
|
setBackground(defaultBackground);
|
||||||
|
|
||||||
|
setAlignment(Pos.TOP_LEFT);
|
||||||
|
setMinHeight(24);
|
||||||
|
setPrefHeight(USE_COMPUTED_SIZE);
|
||||||
|
setMaxHeight(USE_PREF_SIZE);
|
||||||
setMaxWidth(USE_PREF_SIZE);
|
setMaxWidth(USE_PREF_SIZE);
|
||||||
getChildren().addAll(rangesHBox, internalVBox);
|
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);
|
||||||
|
}
|
||||||
|
configureLoDButton(plusButton);
|
||||||
|
configureLoDButton(minusButton);
|
||||||
|
|
||||||
|
//initialize info hbox
|
||||||
|
infoHBox.setMinWidth(USE_PREF_SIZE);
|
||||||
|
infoHBox.setPadding(new Insets(2, 5, 2, 5));
|
||||||
|
infoHBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
//setup description label
|
||||||
|
|
||||||
|
eventTypeImageView.setImage(getEventType().getFXImage());
|
||||||
|
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);
|
||||||
|
|
||||||
|
Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS));
|
||||||
|
for (Range<Long> 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
|
||||||
|
*/
|
||||||
|
installTooltip();
|
||||||
|
showDescriptionLoDControls(true);
|
||||||
|
toFront();
|
||||||
|
});
|
||||||
|
|
||||||
|
setOnMouseExited((MouseEvent e) -> {
|
||||||
|
showDescriptionLoDControls(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param showControls the value of par
|
* @param showControls the value of par
|
||||||
*/
|
*/
|
||||||
@Override
|
|
||||||
void showDescriptionLoDControls(final boolean showControls) {
|
void showDescriptionLoDControls(final boolean showControls) {
|
||||||
super.showDescriptionLoDControls(showControls);
|
DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
|
||||||
show(getSpacer(), showControls);
|
eventType -> new DropShadow(10, eventType.getColor()));
|
||||||
|
clustersHBox.setEffect(showControls ? dropShadow : null);
|
||||||
|
show(minusButton, showControls);
|
||||||
|
show(plusButton, showControls);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSpanWidths(List<Double> spanWidths) {
|
public void setSpanWidths(List<Double> spanWidths) {
|
||||||
for (int i = 0; i < spanWidths.size(); i++) {
|
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);
|
Double w = spanWidths.get(i);
|
||||||
spanRegion.setPrefWidth(w);
|
spanRegion.setPrefWidth(w);
|
||||||
spanRegion.setMaxWidth(w);
|
spanRegion.setMaxWidth(w);
|
||||||
@ -69,40 +250,346 @@ public class EventStripeNode extends AbstractDetailViewNode<EventStripe, EventSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
EventStripe getStripe() {
|
EventStripe getStripe() {
|
||||||
return getEventBundle();
|
return eventStripe;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@NbBundle.Messages({"# {0} - counts",
|
||||||
HBox getSpanFillNode() {
|
"# {1} - event type",
|
||||||
return rangesHBox;
|
"# {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() {
|
||||||
|
if (tooltip == null) {
|
||||||
|
final Task<String> tooltTipTask = new Task<String>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String call() throws Exception {
|
||||||
|
HashMap<String, Long> hashSetCounts = new HashMap<>();
|
||||||
|
if (!getStripe().getEventIDsWithHashHits().isEmpty()) {
|
||||||
|
hashSetCounts = new HashMap<>();
|
||||||
|
try {
|
||||||
|
for (TimeLineEvent tle : eventsModel.getEventsById(getStripe().getEventIDsWithHashHits())) {
|
||||||
|
Set<String> hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames();
|
||||||
|
for (String hashSetName : hashSetNames) {
|
||||||
|
hashSetCounts.merge(hashSetName, 1L, Long::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (TskCoreException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Long> tagCounts = new HashMap<>();
|
||||||
|
if (getEventStripe().getEventIDsWithTags().isEmpty() == false) {
|
||||||
|
tagCounts.putAll(eventsModel.getTagCountsByTagName(getEventStripe().getEventIDsWithTags()));
|
||||||
|
}
|
||||||
|
|
||||||
|
String hashSetCountsString = hashSetCounts.entrySet().stream()
|
||||||
|
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
String tagCountsString = tagCounts.entrySet().stream()
|
||||||
|
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
return Bundle.EventStripeNode_tooltip_text(getEventStripe().getEventIDs().size(), getEventStripe().getEventType(), getEventStripe().getDescription(),
|
||||||
|
TimeLineController.getZonedFormatter().print(getEventStripe().getStartMillis()),
|
||||||
|
TimeLineController.getZonedFormatter().print(getEventStripe().getEndMillis() + 1000))
|
||||||
|
+ (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString)
|
||||||
|
+ (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void succeeded() {
|
||||||
|
super.succeeded();
|
||||||
|
try {
|
||||||
|
tooltip = new Tooltip(get());
|
||||||
|
Tooltip.install(EventStripeNode.this, tooltip);
|
||||||
|
} catch (InterruptedException | ExecutionException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex);
|
||||||
|
Tooltip.uninstall(EventStripeNode.this, tooltip);
|
||||||
|
tooltip = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chart.getController().monitorTask(tooltTipTask);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
EventStripeNode getNodeForBundle(EventStripe cluster) {
|
||||||
Collection<EventStripe> makeBundlesFromClusters(List<EventCluster> eventClusters) {
|
return new EventStripeNode(chart, cluster, this);
|
||||||
return eventClusters.stream().collect(
|
}
|
||||||
Collectors.toMap(
|
|
||||||
EventCluster::getDescription, //key
|
EventType getEventType() {
|
||||||
EventStripe::new, //value
|
return eventStripe.getEventType();
|
||||||
EventStripe::merge)//merge method
|
}
|
||||||
).values();
|
|
||||||
|
String getDescription() {
|
||||||
|
return eventStripe.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
long getStartMillis() {
|
||||||
|
return eventStripe.getStartMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<EventStripeNode> getSubNodes() {
|
||||||
|
return subNodePane.getChildrenUnmodifiable().stream()
|
||||||
|
.map(t -> (EventStripeNode) t)
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* make a new filter intersecting the global filter with description and
|
||||||
|
* type filters to restrict sub-clusters
|
||||||
*
|
*
|
||||||
* @param showSpans the value of showSpans
|
|
||||||
*/
|
*/
|
||||||
@Override
|
RootFilter getSubClusterFilter() {
|
||||||
void showSpans(final boolean showSpans) {
|
RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf();
|
||||||
rangesHBox.setVisible(showSpans);
|
subClusterFilter.getSubFilters().addAll(
|
||||||
|
new DescriptionFilter(eventStripe.getDescriptionLOD(), eventStripe.getDescription()),
|
||||||
|
new TypeFilter(getEventType()));
|
||||||
|
return subClusterFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
void installTooltip() {
|
* @param w the maximum width the description label should have
|
||||||
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
*/
|
||||||
|
public void setDescriptionWidth(double w) {
|
||||||
|
descrLabel.setMaxWidth(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
EventStripeNode getNodeForBundle(EventStripe cluster) {
|
* apply the 'effect' to visually indicate selection
|
||||||
return new EventStripeNode(cluster, this, getChart());
|
*
|
||||||
|
* @param applied true to apply the selection 'effect', false to remove it
|
||||||
|
*/
|
||||||
|
public void applySelectionEffect(boolean applied) {
|
||||||
|
setBorder(applied ? SELECTION_BORDER : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* apply the 'effect' to visually indicate highlighted nodes
|
||||||
|
*
|
||||||
|
* @param applied true to apply the highlight 'effect', false to remove it
|
||||||
|
*/
|
||||||
|
public synchronized void applyHighlightEffect(boolean applied) {
|
||||||
|
if (applied) {
|
||||||
|
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
|
||||||
|
setBackground(highlightedBackground);
|
||||||
|
} else {
|
||||||
|
descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS
|
||||||
|
setBackground(defaultBackground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DescriptionLOD getDescriptionLoD() {
|
||||||
|
return descLOD.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads sub-bundles at the given Description LOD, continues
|
||||||
|
*
|
||||||
|
* @param requestedDescrLoD
|
||||||
|
* @param expand
|
||||||
|
*/
|
||||||
|
@NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters")
|
||||||
|
private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) {
|
||||||
|
subNodePane.getChildren().clear();
|
||||||
|
if (descLOD.get().withRelativeDetail(relativeDetail) == eventStripe.getDescriptionLOD()) {
|
||||||
|
descLOD.set(eventStripe.getDescriptionLOD());
|
||||||
|
clustersHBox.setVisible(true);
|
||||||
|
chart.setRequiresLayout(true);
|
||||||
|
chart.requestChartLayout();
|
||||||
|
} else {
|
||||||
|
clustersHBox.setVisible(false);
|
||||||
|
|
||||||
|
// make new ZoomParams to query with
|
||||||
|
final RootFilter subClusterFilter = getSubClusterFilter();
|
||||||
|
/*
|
||||||
|
* We need to extend end time because for the query by one second,
|
||||||
|
* because it is treated as an open interval but we want to include
|
||||||
|
* events at exactly the time of the last event in this cluster
|
||||||
|
*/
|
||||||
|
final Interval subClusterSpan = new Interval(eventStripe.getStartMillis(), eventStripe.getEndMillis() + 1000);
|
||||||
|
final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get();
|
||||||
|
final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD());
|
||||||
|
|
||||||
|
Task<Set<EventStripeNode>> loggedTask = new Task<Set<EventStripeNode>>() {
|
||||||
|
|
||||||
|
private volatile DescriptionLOD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail);
|
||||||
|
|
||||||
|
{
|
||||||
|
updateTitle(EventStripeNode_loggedTask_name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<EventStripeNode> call() throws Exception {
|
||||||
|
Collection<EventStripe> 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));
|
||||||
|
|
||||||
|
// 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<EventStripeNode> 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
|
||||||
|
chart.getController().monitorTask(loggedTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getLayoutXCompensation() {
|
||||||
|
return (parentNode != null ? parentNode.getLayoutXCompensation() : 0)
|
||||||
|
+ getBoundsInParent().getMinX();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescriptionVisibility(DescriptionVisibility descrVis) {
|
||||||
|
this.descrVis = descrVis;
|
||||||
|
final int size = eventStripe.getEventIDs().size();
|
||||||
|
|
||||||
|
switch (this.descrVis) {
|
||||||
|
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 = parentNode != null
|
||||||
|
? " ..." + StringUtils.substringAfter(description, parentNode.getDescription())
|
||||||
|
: description;
|
||||||
|
descrLabel.setText(description);
|
||||||
|
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventStripe getEventStripe() {
|
||||||
|
return eventStripe;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> getEventsIDs() {
|
||||||
|
return eventStripe.getEventIDs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* event handler used for mouse events on {@link EventStripeNode}s
|
||||||
|
*/
|
||||||
|
private class MouseClickHandler implements EventHandler<MouseEvent> {
|
||||||
|
|
||||||
|
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(new ExpandClusterAction()));
|
||||||
|
contextMenu.getItems().add(ActionUtils.createMenuItem(new 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,6 @@ import org.sleuthkit.autopsy.timeline.TimeLineController;
|
|||||||
import org.sleuthkit.autopsy.timeline.TimeLineView;
|
import org.sleuthkit.autopsy.timeline.TimeLineView;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewNode;
|
|
||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,8 +90,8 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
|||||||
});
|
});
|
||||||
detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
|
detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
|
||||||
eventsTree.getSelectionModel().clearSelection();
|
eventsTree.getSelectionModel().clearSelection();
|
||||||
detailViewPane.getSelectedNodes().forEach((DetailViewNode<?> t) -> {
|
detailViewPane.getSelectedNodes().forEach(eventStripeNode -> {
|
||||||
eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEventBundle()));
|
eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(eventStripeNode.getEventStripe()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user