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)