19 package org.sleuthkit.autopsy.timeline.ui.detailview;
 
   21 import com.google.common.collect.Iterables;
 
   22 import java.util.ArrayList;
 
   23 import java.util.Arrays;
 
   24 import java.util.Collections;
 
   25 import java.util.List;
 
   26 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;
 
   71 final 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) {
 
  103         super(chartLane, eventCluster, 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);
 
  115         if (parentNode == null) {
 
  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);
 
  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);
 
  274     protected void layoutChildren() {
 
  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(
 
  285                 Arrays.asList(
new ExpandClusterAction(
this), 
new CollapseClusterAction(
this))
 
  290     EventHandler<MouseEvent> getDoubleClickHandler() {
 
  291         return mouseEvent -> 
new ExpandClusterAction(
this).handle(null);
 
  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) {
 
  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()));
 
  350         switch (relativeDetail) {
 
  358                 throw new IllegalArgumentException(
"Unknown RelativeDetail value " + relativeDetail);
 
synchronized RootFilterState getEventFilterState()
Optional< EventNodeBase<?> > getParentNode()
TimelineLevelOfDetail moreDetailed()
synchronized TimelineEventType.HierarchyLevel getEventTypeZoom()
TimeLineController getController()
EventsModel getEventsModel()
RootFilterState intersect(FilterState< ?extends TimelineFilter > other)
TimelineLevelOfDetail lessDetailed()
TimelineEvent getEventById(Long eventID)