19package org.sleuthkit.autopsy.timeline.ui.detailview;
21import com.google.common.collect.Iterables;
22import java.util.ArrayList;
23import java.util.Arrays;
24import java.util.Collections;
26import static java.util.Objects.nonNull;
28import java.util.concurrent.ExecutionException;
29import java.util.logging.Level;
30import java.util.stream.Collectors;
31import javafx.collections.ObservableList;
32import javafx.concurrent.Task;
33import javafx.event.EventHandler;
34import javafx.geometry.Pos;
35import javafx.scene.Cursor;
36import javafx.scene.control.Button;
37import javafx.scene.image.Image;
38import javafx.scene.image.ImageView;
39import javafx.scene.input.MouseEvent;
40import javafx.scene.layout.Border;
41import javafx.scene.layout.BorderStroke;
42import javafx.scene.layout.BorderStrokeStyle;
43import javafx.scene.layout.BorderWidths;
44import javafx.scene.layout.VBox;
45import org.controlsfx.control.action.Action;
46import org.controlsfx.control.action.ActionUtils;
47import org.joda.time.DateTime;
48import org.joda.time.Interval;
49import org.openide.util.NbBundle;
50import org.sleuthkit.autopsy.coreutils.LoggedTask;
51import org.sleuthkit.autopsy.coreutils.Logger;
52import org.sleuthkit.autopsy.coreutils.ThreadConfined;
53import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configureActionButton;
54import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.DetailViewEvent;
55import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventCluster;
56import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventStripe;
57import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.SingleDetailsViewEvent;
58import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.SqlFilterState;
59import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilter;
60import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
61import org.sleuthkit.autopsy.timeline.zooming.EventsModelParams;
62import org.sleuthkit.datamodel.TimelineLevelOfDetail;
63import org.sleuthkit.datamodel.TskCoreException;
64import org.sleuthkit.datamodel.TimelineEventType;
65import org.sleuthkit.datamodel.TimelineEvent;
66import org.sleuthkit.datamodel.TimelineFilter.EventTypeFilter;
71final class EventClusterNode
extends MultiEventNodeBase<EventCluster, EventStripe, EventStripeNode> {
73 private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
78 private static final BorderWidths CLUSTER_BORDER_WIDTHS =
new BorderWidths(2, 1, 2, 1);
84 private final Border clusterBorder =
new Border(
new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS));
89 private Button plusButton;
93 private Button minusButton;
102 EventClusterNode(DetailsChartLane<?> chartLane, EventCluster eventCluster, EventStripeNode
parentNode) {
105 subNodePane.setBorder(clusterBorder);
106 subNodePane.setBackground(defaultBackground);
107 subNodePane.setMinWidth(1);
108 subNodePane.setMaxWidth(USE_PREF_SIZE);
110 setAlignment(Pos.CENTER_LEFT);
112 setCursor(Cursor.HAND);
113 getChildren().addAll(subNodePane, infoHBox);
116 setDescriptionVisibility(DescriptionVisibility.SHOWN);
125 Button getNewExpandButton() {
126 return ActionUtils.createButton(
new ExpandClusterAction(
this), ActionUtils.ActionTextBehavior.HIDE);
134 Button getNewCollapseButton() {
135 return ActionUtils.createButton(
new CollapseClusterAction(
this), ActionUtils.ActionTextBehavior.HIDE);
139 void installActionButtons() {
140 super.installActionButtons();
141 if (plusButton ==
null) {
142 plusButton = getNewExpandButton();
143 minusButton = getNewCollapseButton();
144 controlsHBox.getChildren().addAll(minusButton, plusButton);
146 configureActionButton(plusButton);
147 configureActionButton(minusButton);
152 void showFullDescription(
final int size) {
156 super.showFullDescription(size);
166 @NbBundle.Messages(value =
"EventClusterNode.loggedTask.name=Load sub events")
167 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
168 private synchronized
void loadSubStripes(
RelativeDetail relativeDetail) {
169 getChartLane().setCursor(Cursor.WAIT);
180 RootFilterState subClusterFilter = eventsModel.getEventFilterState()
181 .intersect(
new SqlFilterState<>(
182 new EventTypeFilter(getEventType()),
true));
183 final Interval subClusterSpan =
new Interval(getStartMillis(), getEndMillis() + 1000);
184 final TimelineEventType.HierarchyLevel eventTypeZoomLevel = eventsModel.getEventTypeZoom();
185 final EventsModelParams zoom =
new EventsModelParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLevel());
187 DescriptionFilter descriptionFilter =
new DescriptionFilter(
getEvent().getDescriptionLevel(), getDescription());
191 Task<List<EventStripe>> loggedTask;
192 loggedTask =
new LoggedTask<List<EventStripe>>(Bundle.EventClusterNode_loggedTask_name(),
false) {
194 private volatile TimelineLevelOfDetail loadedDescriptionLevel = withRelativeDetail(getDescriptionLevel(), relativeDetail);
197 protected List<EventStripe> call() throws Exception {
199 List<EventStripe> stripes;
201 TimelineLevelOfDetail next = loadedDescriptionLevel;
203 loadedDescriptionLevel = next;
204 if (loadedDescriptionLevel ==
getEvent().getDescriptionLevel()) {
206 return Collections.emptyList();
210 stripes = chartLane.getParentChart().getDetailsViewModel().getEventStripes(descriptionFilter, zoom.withDescrLOD(loadedDescriptionLevel));
212 next = withRelativeDetail(loadedDescriptionLevel, relativeDetail);
213 }
while (stripes.size() == 1 && nonNull(next));
216 return stripes.stream()
217 .map(eventStripe -> eventStripe.withParent(
getEvent()))
218 .collect(Collectors.toList());
222 protected void succeeded() {
223 ObservableList<DetailViewEvent> chartNestedEvents = getChartLane().getParentChart().getAllNestedEvents();
226 chartNestedEvents.removeAll(StripeFlattener.flatten(subNodes));
230 setDescriptionLOD(loadedDescriptionLevel);
231 List<EventStripe> newSubStripes =
get();
232 if (newSubStripes.isEmpty()) {
234 getChildren().setAll(subNodePane, infoHBox);
237 List<EventNodeBase<?>> newSubNodes =
new ArrayList<>();
238 for (EventStripe subStripe : newSubStripes) {
239 newSubNodes.add(createChildNode(subStripe));
241 subNodes.addAll(newSubNodes);
242 chartNestedEvents.addAll(StripeFlattener.flatten(subNodes));
243 getChildren().setAll(
new VBox(infoHBox, subNodePane));
245 }
catch (TskCoreException | InterruptedException | ExecutionException ex) {
246 LOGGER.log(Level.SEVERE,
"Error loading subnodes", ex);
250 getChartLane().requestChartLayout();
251 getChartLane().setCursor(
null);
256 new Thread(loggedTask).start();
257 getChartLane().getController().monitorTask(loggedTask);
261 EventNodeBase<?> createChildNode(EventStripe stripe)
throws TskCoreException {
262 Set<Long> eventIDs = stripe.getEventIDs();
263 if (eventIDs.size() == 1) {
266 SingleDetailsViewEvent singleDetailsEvent =
new SingleDetailsViewEvent(singleEvent).withParent(stripe);
267 return new SingleEventNode(getChartLane(), singleDetailsEvent,
this);
269 return new EventStripeNode(getChartLane(), stripe,
this);
275 double chartX = getChartLane().getXAxis().getDisplayPosition(
new DateTime(getStartMillis()));
276 double width = getChartLane().getXAxis().getDisplayPosition(
new DateTime(getEndMillis())) - chartX;
277 subNodePane.setPrefWidth(Math.max(1, width));
278 super.layoutChildren();
282 Iterable<? extends Action> getActions() {
283 return Iterables.concat(
290 EventHandler<MouseEvent> getDoubleClickHandler() {
298 static private class ExpandClusterAction
extends Action {
300 private static final Image
PLUS =
new Image(
"/org/sleuthkit/autopsy/timeline/images/plus-button.png");
302 @NbBundle.Messages({
"ExpandClusterAction.text=Expand"})
304 super(Bundle.ExpandClusterAction_text());
305 setGraphic(
new ImageView(
PLUS));
307 setEventHandler(actionEvent -> {
308 if (node.getDescriptionLevel().moreDetailed() !=
null) {
314 disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(TimelineLevelOfDetail.HIGH));
322 static private class CollapseClusterAction
extends Action {
324 private static final Image
MINUS =
new Image(
"/org/sleuthkit/autopsy/timeline/images/minus-button.png");
326 @NbBundle.Messages({
"CollapseClusterAction.text=Collapse"})
328 super(Bundle.CollapseClusterAction_text());
329 setGraphic(
new ImageView(
MINUS));
331 setEventHandler(actionEvent -> {
332 if (node.getDescriptionLevel().lessDetailed() !=
null) {
338 disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(node.getEvent().getDescriptionLevel()));
349 private static TimelineLevelOfDetail withRelativeDetail(TimelineLevelOfDetail LoD,
RelativeDetail relativeDetail) {
350 switch (relativeDetail) {
354 return LoD.moreDetailed();
356 return LoD.lessDetailed();
358 throw new IllegalArgumentException(
"Unknown RelativeDetail value " + relativeDetail);
TimelineEvent getEventById(Long eventID)
EventsModel getEventsModel()
final EventNodeBase<?> parentNode
Optional< EventNodeBase<?> > getParentNode()
TimeLineController getController()