20 package org.sleuthkit.autopsy.timeline.ui.detailview;
 
   22 import com.google.common.collect.Lists;
 
   23 import com.google.common.collect.Sets;
 
   24 import com.google.common.eventbus.Subscribe;
 
   25 import java.util.Arrays;
 
   26 import java.util.Collection;
 
   27 import java.util.List;
 
   29 import java.util.Optional;
 
   30 import java.util.concurrent.ConcurrentHashMap;
 
   31 import java.util.concurrent.ExecutionException;
 
   32 import java.util.logging.Level;
 
   33 import javafx.animation.KeyFrame;
 
   34 import javafx.animation.KeyValue;
 
   35 import javafx.animation.Timeline;
 
   36 import javafx.application.Platform;
 
   37 import javafx.concurrent.Task;
 
   38 import javafx.event.EventHandler;
 
   39 import javafx.geometry.Insets;
 
   40 import javafx.scene.Node;
 
   41 import javafx.scene.control.Button;
 
   42 import javafx.scene.control.ButtonBase;
 
   43 import javafx.scene.control.ContextMenu;
 
   44 import javafx.scene.control.Label;
 
   45 import javafx.scene.control.SeparatorMenuItem;
 
   46 import javafx.scene.control.Tooltip;
 
   47 import javafx.scene.effect.DropShadow;
 
   48 import javafx.scene.effect.Effect;
 
   49 import javafx.scene.image.Image;
 
   50 import javafx.scene.image.ImageView;
 
   51 import javafx.scene.input.MouseButton;
 
   52 import javafx.scene.input.MouseEvent;
 
   53 import javafx.scene.layout.Background;
 
   54 import javafx.scene.layout.BackgroundFill;
 
   55 import javafx.scene.layout.Border;
 
   56 import javafx.scene.layout.BorderStroke;
 
   57 import javafx.scene.layout.BorderStrokeStyle;
 
   58 import javafx.scene.layout.BorderWidths;
 
   59 import javafx.scene.layout.HBox;
 
   60 import javafx.scene.layout.StackPane;
 
   61 import javafx.scene.paint.Color;
 
   62 import javafx.util.Duration;
 
   63 import org.apache.commons.lang3.StringUtils;
 
   64 import org.controlsfx.control.action.Action;
 
   65 import org.controlsfx.control.action.ActionUtils;
 
   66 import org.joda.time.DateTime;
 
   67 import org.openide.util.NbBundle;
 
   91     private static final Image 
HASH_HIT = 
new Image(
"/org/sleuthkit/autopsy/images/hashset_hits.png"); 
 
   92     private static final Image 
TAG = 
new Image(
"/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); 
 
   93     private static final Image 
PIN = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/marker--plus.png"); 
 
   94     private static final Image 
UNPIN = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/marker--minus.png"); 
 
   96     private static final Map<TimelineEventType, Effect> 
dropShadowMap = 
new ConcurrentHashMap<>();
 
   98     static void configureActionButton(ButtonBase b) {
 
  100         b.setMaxSize(16, 16);
 
  101         b.setPrefSize(16, 16);
 
  104     static void show(Node b, 
boolean show) {
 
  113     final DetailsChartLane<?> chartLane;
 
  114     final Background highlightedBackground;
 
  115     final Background defaultBackground;
 
  116     final Color evtColor;
 
  118     final Label countLabel = 
new Label();
 
  119     final Label descrLabel = 
new Label();
 
  120     final ImageView hashIV = 
new ImageView(HASH_HIT);
 
  121     final ImageView tagIV = 
new ImageView(TAG);
 
  122     final ImageView eventTypeImageView = 
new ImageView();
 
  124     final Tooltip tooltip = 
new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading());
 
  126     final HBox controlsHBox = 
new HBox(5);
 
  127     final HBox infoHBox = 
new HBox(5, eventTypeImageView, hashIV, tagIV, descrLabel, countLabel, controlsHBox);
 
  128     final SleuthkitCase sleuthkitCase;
 
  135         this.chartLane = chartLane;
 
  137         this.parentNode = parent;
 
  139         sleuthkitCase = chartLane.getController().getAutopsyCase().getSleuthkitCase();
 
  140         eventsModel = chartLane.getController().getEventsModel();
 
  141         eventTypeImageView.setImage(
new Image(getImagePath(getEventType())));
 
  143         if (tlEvent.getEventIDsWithHashHits().isEmpty()) {
 
  147         if (tlEvent.getEventIDsWithTags().isEmpty()) {
 
  151         if (chartLane.getController().getEventsModel().getEventTypeZoom() == TimelineEventType.HierarchyLevel.CATEGORY) {
 
  152             evtColor = getColor(getEventType());
 
  154             evtColor = getColor(getEventType().getCategory());
 
  156         SELECTION_BORDER = 
new Border(
new BorderStroke(evtColor.darker().desaturate(), BorderStrokeStyle.SOLID, CORNER_RADII_3, 
new BorderWidths(2)));
 
  158         defaultBackground = 
new Background(
new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY));
 
  159         highlightedBackground = 
new Background(
new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY));
 
  160         setBackground(defaultBackground);
 
  162         Tooltip.install(
this, this.tooltip);
 
  165         setOnMouseEntered(mouseEntered -> {
 
  167             showHoverControls(
true);
 
  171         setOnMouseExited(mouseExited -> {
 
  172             showHoverControls(
false);
 
  173             if (parentNode != null) {
 
  174                 parentNode.showHoverControls(
true);
 
  179         setOnMouseClicked(
new ClickHandler());
 
  180         show(controlsHBox, 
false);
 
  189         return chartLane.getController();
 
  193         return Optional.ofNullable(parentNode);
 
  196     DetailsChartLane<?> getChartLane() {
 
  204         descrLabel.setMaxWidth(w);
 
  207     public abstract List<EventNodeBase<?>> 
getSubNodes();
 
  215         setBorder(applied ? SELECTION_BORDER : null);
 
  220         super.layoutChildren();
 
  228     void installActionButtons() {
 
  229         if (pinButton == null) {
 
  230             pinButton = 
new Button();
 
  231             controlsHBox.getChildren().add(pinButton);
 
  232             configureActionButton(pinButton);
 
  236     final void showHoverControls(
final boolean showControls) {
 
  237         Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
 
  238                 eventType -> 
new DropShadow(-10, getColor(eventType)));
 
  239         setEffect(showControls ? dropShadow : null);
 
  241         enableTooltip(showControls);
 
  242         installActionButtons();
 
  247             pinButton.setOnAction(actionEvent -> {
 
  248                 new UnPinEventAction(controller, tlEvent).handle(actionEvent);
 
  249                 showHoverControls(
true);
 
  251             pinButton.setGraphic(
new ImageView(UNPIN));
 
  253             pinButton.setOnAction(actionEvent -> {
 
  254                 new PinEventAction(controller, tlEvent).handle(actionEvent);
 
  255                 showHoverControls(
true);
 
  257             pinButton.setGraphic(
new ImageView(PIN));
 
  260         show(controlsHBox, showControls);
 
  261         if (parentNode != null) {
 
  262             parentNode.showHoverControls(
false);
 
  270     @NbBundle.Messages({
"# {0} - counts",
 
  271         "# {1} - event type",
 
  272         "# {2} - description",
 
  273         "# {3} - start date/time",
 
  274         "# {4} - end date/time",
 
  275         "EventNodeBase.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand   \t{4}",
 
  276         "EventNodeBase.toolTip.loading2=loading tooltip",
 
  277         "# {0} - hash set count string",
 
  278         "EventNodeBase.toolTip.hashSetHits=\n\nHash Set Hits\n{0}",
 
  279         "# {0} - tag count string",
 
  280         "EventNodeBase.toolTip.tags=\n\nTags\n{0}"})
 
  281     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  282     void installTooltip() {
 
  283         if (tooltip.getText().equalsIgnoreCase(Bundle.EventBundleNodeBase_toolTip_loading())) {
 
  284             final Task<String> tooltTipTask = 
new Task<String>() {
 
  286                     updateTitle(Bundle.EventNodeBase_toolTip_loading2());
 
  290                 protected String call() throws Exception {
 
  291                     return Bundle.EventNodeBase_tooltip_text(getEventIDs().size(), getEventType(), getDescription(),
 
  292                             TimeLineController.getZonedFormatter().print(getStartMillis()),
 
  293                             TimeLineController.getZonedFormatter().print(getEndMillis() + 1000));
 
  297                 protected void done() {
 
  300                         tooltip.setText(
get());
 
  301                         tooltip.setGraphic(null);
 
  302                     } 
catch (InterruptedException | ExecutionException ex) {
 
  303                         LOGGER.log(Level.SEVERE, 
"Tooltip generation failed.", ex); 
 
  307             new Thread(tooltTipTask).start();
 
  308             chartLane.getController().monitorTask(tooltTipTask);
 
  312     void enableTooltip(
boolean toolTipEnabled) {
 
  313         if (toolTipEnabled) {
 
  314             Tooltip.install(
this, tooltip);
 
  316             Tooltip.uninstall(
this, tooltip);
 
  320     final TimelineEventType getEventType() {
 
  321         return tlEvent.getEventType();
 
  324     long getStartMillis() {
 
  325         return tlEvent.getStartMillis();
 
  328     final long getEndMillis() {
 
  329         return tlEvent.getEndMillis();
 
  332     final double getLayoutXCompensation() {
 
  333         return parentNode != null
 
  334                 ? getChartLane().getXAxis().getDisplayPosition(
new DateTime(parentNode.getStartMillis()))
 
  338     abstract String getDescription();
 
  340     void animateTo(
double xLeft, 
double yTop) {
 
  341         if (timeline != null) {
 
  343             Platform.runLater(this::requestChartLayout);
 
  345         timeline = 
new Timeline(
new KeyFrame(Duration.millis(100),
 
  346                 new KeyValue(layoutXProperty(), xLeft),
 
  347                 new KeyValue(layoutYProperty(), yTop))
 
  349         timeline.setOnFinished(finished -> Platform.runLater(this::requestChartLayout));
 
  353     void requestChartLayout() {
 
  354         getChartLane().requestChartLayout();
 
  357     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  358     void setDescriptionVisibility(DescriptionVisibility descrVis) {
 
  359         final int size = 
getEvent().getSize();
 
  369                 showFullDescription(size);
 
  374     void showCountOnly(
final int size) {
 
  375         descrLabel.setText(
"");
 
  376         countLabel.setText(String.valueOf(size));
 
  379     void hideDescription() {
 
  380         countLabel.setText(
"");
 
  381         descrLabel.setText(
"");
 
  389     synchronized void applyHighlightEffect(
boolean applied) {
 
  391             descrLabel.setStyle(
"-fx-font-weight: bold;"); 
 
  392             setBackground(highlightedBackground);
 
  394             descrLabel.setStyle(
"-fx-font-weight: normal;"); 
 
  395             setBackground(defaultBackground);
 
  399     void applyHighlightEffect() {
 
  400         applyHighlightEffect(
true);
 
  403     void clearHighlightEffect() {
 
  404         applyHighlightEffect(
false);
 
  407     abstract Collection<Long> getEventIDs();
 
  409     abstract EventHandler<MouseEvent> getDoubleClickHandler();
 
  411     Iterable<? extends Action> getActions() {
 
  425         ContextMenu chartContextMenu = chartLane.getContextMenu(mouseEvent);
 
  427         ContextMenu contextMenu = ActionUtils.createContextMenu(Lists.newArrayList(getActions()));
 
  428         contextMenu.getItems().add(
new SeparatorMenuItem());
 
  429         contextMenu.getItems().addAll(chartContextMenu.getItems());
 
  430         contextMenu.setAutoHide(
true);
 
  434     void showFullDescription(
final int size) {
 
  435         countLabel.setText((size == 1) ? 
"" : 
" (" + size + 
")"); 
 
  437                 -> 
"    ..." + StringUtils.substringAfter(
getEvent().getDescription(), parentNode.getDescription()))
 
  438                 .orElseGet(
getEvent()::getDescription);
 
  440         descrLabel.setText(description);
 
  446             Platform.runLater(() -> {
 
  459         if (
false == difference.isEmpty()) {
 
  460             Platform.runLater(() -> {
 
  468         @NbBundle.Messages({
"PinEventAction.text=Pin"})
 
  470             super(Bundle.PinEventAction_text());
 
  471             setEventHandler(actionEvent -> controller.
pinEvent(event));
 
  472             setGraphic(
new ImageView(PIN));
 
  478         @NbBundle.Messages({
"UnPinEventAction.text=Unpin"})
 
  480             super(Bundle.UnPinEventAction_text());
 
  481             setEventHandler(actionEvent -> controller.
unPinEvent(event));
 
  482             setGraphic(
new ImageView(UNPIN));
 
  493             if (t.getButton() == MouseButton.PRIMARY) {
 
  494                 if (t.getClickCount() > 1) {
 
  495                     getDoubleClickHandler().handle(t);
 
  496                 } 
else if (t.isShiftDown()) {
 
  498                 } 
else if (t.isShortcutDown()) {
 
  504             } 
else if (t.isPopupTrigger() && t.isStillSincePress()) {
 
void unPinEvent(DetailViewEvent event)
 
static final Image HASH_HIT
 
final Border SELECTION_BORDER
 
Optional< EventNodeBase<?> > getParentNode()
 
static Tooltip getDefaultTooltip()
 
void setMaxDescriptionWidth(double w)
 
void handleTimeLineTagEvent(TagsAddedEvent event)
 
final EventNodeBase<?> parentNode
 
abstract List< EventNodeBase<?> > getSubNodes()
 
final void clearContextMenu()
 
ContextMenu getContextMenu(MouseEvent mouseEvent)
 
static final Map< TimelineEventType, Effect > dropShadowMap
 
static final Logger LOGGER
 
void handleTimeLineTagEvent(TagsDeletedEvent event)
 
TimeLineController getController()
 
void applySelectionEffect(boolean applied)
 
synchronized static Logger getLogger(String name)
 
static String getImagePath(TimelineEventType type)
 
void pinEvent(DetailViewEvent event)
 
ObservableSet< DetailViewEvent > getPinnedEvents()
 
void handle(MouseEvent t)
 
static Color getColor(TimelineEventType type)