19 package org.sleuthkit.autopsy.timeline.ui.detailview;
21 import com.google.common.collect.ImmutableSet;
22 import com.google.common.collect.Iterables;
23 import com.google.common.collect.Lists;
24 import java.util.Arrays;
25 import java.util.Collections;
26 import java.util.List;
27 import static java.util.Objects.nonNull;
28 import java.util.concurrent.ExecutionException;
29 import java.util.logging.Level;
30 import java.util.stream.Collectors;
31 import javafx.collections.ObservableList;
32 import javafx.concurrent.Task;
33 import javafx.event.EventHandler;
34 import javafx.geometry.Pos;
35 import javafx.scene.Cursor;
36 import javafx.scene.control.Button;
37 import javafx.scene.image.Image;
38 import javafx.scene.image.ImageView;
39 import javafx.scene.input.MouseEvent;
40 import javafx.scene.layout.Border;
41 import javafx.scene.layout.BorderStroke;
42 import javafx.scene.layout.BorderStrokeStyle;
43 import javafx.scene.layout.BorderWidths;
44 import javafx.scene.layout.VBox;
45 import org.controlsfx.control.action.Action;
46 import org.controlsfx.control.action.ActionUtils;
47 import org.joda.time.DateTime;
48 import org.joda.time.Interval;
49 import org.openide.util.NbBundle;
68 final class EventClusterNode
extends MultiEventNodeBase<EventCluster, EventStripe, EventStripeNode> {
70 private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
75 private static final BorderWidths CLUSTER_BORDER_WIDTHS =
new BorderWidths(2, 1, 2, 1);
81 private final Border clusterBorder =
new Border(
new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS));
86 private Button plusButton;
90 private Button minusButton;
99 EventClusterNode(DetailsChartLane<?> chartLane, EventCluster eventCluster, EventStripeNode parentNode) {
100 super(chartLane, eventCluster, parentNode);
102 subNodePane.setBorder(clusterBorder);
103 subNodePane.setBackground(defaultBackground);
104 subNodePane.setMinWidth(1);
105 subNodePane.setMaxWidth(USE_PREF_SIZE);
107 setAlignment(Pos.CENTER_LEFT);
109 setCursor(Cursor.HAND);
110 getChildren().addAll(subNodePane, infoHBox);
112 if (parentNode == null) {
113 setDescriptionVisibility(DescriptionVisibility.SHOWN);
122 Button getNewExpandButton() {
123 return ActionUtils.createButton(
new ExpandClusterAction(
this), ActionUtils.ActionTextBehavior.HIDE);
131 Button getNewCollapseButton() {
132 return ActionUtils.createButton(
new CollapseClusterAction(
this), ActionUtils.ActionTextBehavior.HIDE);
136 void installActionButtons() {
137 super.installActionButtons();
138 if (plusButton == null) {
139 plusButton = getNewExpandButton();
140 minusButton = getNewCollapseButton();
141 controlsHBox.getChildren().addAll(minusButton, plusButton);
143 configureActionButton(plusButton);
144 configureActionButton(minusButton);
149 void showFullDescription(
final int size) {
150 if (getParentNode().isPresent()) {
153 super.showFullDescription(size);
163 @NbBundle.Messages(value =
"EventClusterNode.loggedTask.name=Load sub events")
164 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
165 private synchronized
void loadSubStripes(DescriptionLoD.RelativeDetail relativeDetail) {
166 getChartLane().setCursor(Cursor.WAIT);
177 final RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf();
178 subClusterFilter.getSubFilters().addAll(
179 new DescriptionFilter(getEvent().getDescriptionLoD(), getDescription(), DescriptionFilter.FilterMode.INCLUDE),
180 new TypeFilter(getEventType()));
181 final Interval subClusterSpan =
new Interval(getStartMillis(), getEndMillis() + 1000);
182 final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get();
183 final ZoomParams zoomParams =
new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD());
188 Task<List<EventStripe>> loggedTask;
189 loggedTask =
new LoggedTask<List<EventStripe>>(Bundle.EventClusterNode_loggedTask_name(),
false) {
191 private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().
withRelativeDetail(relativeDetail);
194 protected List<EventStripe> call() throws Exception {
196 List<EventStripe> stripes;
198 DescriptionLoD next = loadedDescriptionLoD;
201 loadedDescriptionLoD = next;
202 if (loadedDescriptionLoD == getEvent().getDescriptionLoD()) {
204 return Collections.emptyList();
208 stripes = eventsModel.getEventStripes(zoomParams.withDescrLOD(loadedDescriptionLoD));
211 }
while (stripes.size() == 1 && nonNull(next));
214 return stripes.stream()
215 .map(eventStripe -> eventStripe.withParent(getEvent()))
216 .collect(Collectors.toList());
220 protected void succeeded() {
221 ObservableList<TimeLineEvent> chartNestedEvents = getChartLane().getParentChart().getAllNestedEvents();
224 chartNestedEvents.removeAll(StripeFlattener.flatten(subNodes));
228 setDescriptionLOD(loadedDescriptionLoD);
229 List<EventStripe> newSubStripes =
get();
230 if (newSubStripes.isEmpty()) {
232 getChildren().setAll(subNodePane, infoHBox);
235 subNodes.addAll(Lists.transform(newSubStripes, EventClusterNode.this::createChildNode));
236 chartNestedEvents.addAll(StripeFlattener.flatten(subNodes));
237 getChildren().setAll(
new VBox(infoHBox, subNodePane));
239 }
catch (InterruptedException | ExecutionException ex) {
240 LOGGER.log(Level.SEVERE,
"Error loading subnodes", ex);
243 getChartLane().requestChartLayout();
244 getChartLane().setCursor(null);
249 new Thread(loggedTask).start();
250 getChartLane().getController().monitorTask(loggedTask);
255 ImmutableSet<Long> eventIDs = stripe.getEventIDs();
256 if (eventIDs.size() == 1) {
258 SingleEvent singleEvent = getController().getEventsModel().getEventById(Iterables.getOnlyElement(eventIDs)).withParent(stripe);
259 return new SingleEventNode(getChartLane(), singleEvent,
this);
261 return new EventStripeNode(getChartLane(), stripe,
this);
266 protected void layoutChildren() {
267 double chartX = getChartLane().getXAxis().getDisplayPosition(
new DateTime(getStartMillis()));
268 double w = getChartLane().getXAxis().getDisplayPosition(
new DateTime(getEndMillis())) - chartX;
269 subNodePane.setPrefWidth(Math.max(1, w));
270 super.layoutChildren();
274 Iterable<? extends Action> getActions() {
275 return Iterables.concat(
277 Arrays.asList(
new ExpandClusterAction(
this),
new CollapseClusterAction(
this))
282 EventHandler<MouseEvent> getDoubleClickHandler() {
283 return mouseEvent ->
new ExpandClusterAction(
this).handle(null);
292 private static final Image
PLUS =
new Image(
"/org/sleuthkit/autopsy/timeline/images/plus-button.png");
294 @NbBundle.Messages({
"ExpandClusterAction.text=Expand"})
296 super(Bundle.ExpandClusterAction_text());
297 setGraphic(
new ImageView(PLUS));
299 setEventHandler(actionEvent -> {
300 if (node.getDescriptionLoD().moreDetailed() != null) {
306 disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(
DescriptionLoD.
FULL));
316 private static final Image
MINUS =
new Image(
"/org/sleuthkit/autopsy/timeline/images/minus-button.png");
318 @NbBundle.Messages({
"CollapseClusterAction.text=Collapse"})
320 super(Bundle.CollapseClusterAction_text());
321 setGraphic(
new ImageView(MINUS));
323 setEventHandler(actionEvent -> {
324 if (node.getDescriptionLoD().lessDetailed() != null) {
330 disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(node.getEvent().getDescriptionLoD()));
DescriptionLoD withRelativeDetail(RelativeDetail relativeDetail)