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)