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.HashMap;
28 import java.util.List;
30 import java.util.Optional;
32 import java.util.concurrent.ConcurrentHashMap;
33 import java.util.concurrent.ExecutionException;
34 import java.util.logging.Level;
35 import java.util.stream.Collectors;
36 import javafx.animation.KeyFrame;
37 import javafx.animation.KeyValue;
38 import javafx.animation.Timeline;
39 import javafx.application.Platform;
40 import javafx.concurrent.Task;
41 import javafx.event.EventHandler;
42 import javafx.geometry.Insets;
43 import javafx.scene.Node;
44 import javafx.scene.control.Button;
45 import javafx.scene.control.ButtonBase;
46 import javafx.scene.control.ContextMenu;
47 import javafx.scene.control.Label;
48 import javafx.scene.control.SeparatorMenuItem;
49 import javafx.scene.control.Tooltip;
50 import javafx.scene.effect.DropShadow;
51 import javafx.scene.effect.Effect;
52 import javafx.scene.image.Image;
53 import javafx.scene.image.ImageView;
54 import javafx.scene.input.MouseButton;
55 import javafx.scene.input.MouseEvent;
56 import javafx.scene.layout.Background;
57 import javafx.scene.layout.BackgroundFill;
58 import javafx.scene.layout.Border;
59 import javafx.scene.layout.BorderStroke;
60 import javafx.scene.layout.BorderStrokeStyle;
61 import javafx.scene.layout.BorderWidths;
62 import javafx.scene.layout.HBox;
63 import javafx.scene.layout.StackPane;
64 import javafx.scene.paint.Color;
65 import javafx.util.Duration;
66 import org.apache.commons.lang3.StringUtils;
67 import org.controlsfx.control.action.Action;
68 import org.controlsfx.control.action.ActionUtils;
69 import org.joda.time.DateTime;
70 import org.openide.util.NbBundle;
96 private static final Image
HASH_HIT =
new Image(
"/org/sleuthkit/autopsy/images/hashset_hits.png");
97 private static final Image
TAG =
new Image(
"/org/sleuthkit/autopsy/images/green-tag-icon-16.png");
98 private static final Image
PIN =
new Image(
"/org/sleuthkit/autopsy/timeline/images/marker--plus.png");
99 private static final Image
UNPIN =
new Image(
"/org/sleuthkit/autopsy/timeline/images/marker--minus.png");
101 private static final Map<TimelineEventType, Effect>
dropShadowMap =
new ConcurrentHashMap<>();
103 static void configureActionButton(ButtonBase b) {
104 b.setMinSize(16, 16);
105 b.setMaxSize(16, 16);
106 b.setPrefSize(16, 16);
109 static void show(Node b,
boolean show) {
118 final DetailsChartLane<?> chartLane;
119 final Background highlightedBackground;
120 final Background defaultBackground;
121 final Color evtColor;
123 final Label countLabel =
new Label();
124 final Label descrLabel =
new Label();
125 final ImageView hashIV =
new ImageView(HASH_HIT);
126 final ImageView tagIV =
new ImageView(TAG);
127 final ImageView eventTypeImageView =
new ImageView();
129 final Tooltip tooltip =
new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading());
131 final HBox controlsHBox =
new HBox(5);
132 final HBox infoHBox =
new HBox(5, eventTypeImageView, hashIV, tagIV, descrLabel, countLabel, controlsHBox);
133 final SleuthkitCase sleuthkitCase;
140 this.chartLane = chartLane;
142 this.parentNode = parent;
144 sleuthkitCase = chartLane.getController().getAutopsyCase().getSleuthkitCase();
145 eventsModel = chartLane.getController().getEventsModel();
146 eventTypeImageView.setImage(
new Image(getImagePath(getEventType())));
148 if (tlEvent.getEventIDsWithHashHits().isEmpty()) {
152 if (tlEvent.getEventIDsWithTags().isEmpty()) {
156 if (chartLane.getController().getEventsModel().getEventTypeZoom() == TimelineEventType.TypeLevel.SUB_TYPE) {
157 evtColor = getColor(getEventType());
159 evtColor = getColor(getEventType().getBaseType());
161 SELECTION_BORDER =
new Border(
new BorderStroke(evtColor.darker().desaturate(), BorderStrokeStyle.SOLID, CORNER_RADII_3,
new BorderWidths(2)));
163 defaultBackground =
new Background(
new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY));
164 highlightedBackground =
new Background(
new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY));
165 setBackground(defaultBackground);
167 Tooltip.install(
this, this.tooltip);
170 setOnMouseEntered(mouseEntered -> {
172 showHoverControls(
true);
176 setOnMouseExited(mouseExited -> {
177 showHoverControls(
false);
178 if (parentNode != null) {
179 parentNode.showHoverControls(
true);
184 setOnMouseClicked(
new ClickHandler());
185 show(controlsHBox,
false);
194 return chartLane.getController();
198 return Optional.ofNullable(parentNode);
201 DetailsChartLane<?> getChartLane() {
209 descrLabel.setMaxWidth(w);
212 public abstract List<EventNodeBase<?>>
getSubNodes();
220 setBorder(applied ? SELECTION_BORDER : null);
225 super.layoutChildren();
233 void installActionButtons() {
234 if (pinButton == null) {
235 pinButton =
new Button();
236 controlsHBox.getChildren().add(pinButton);
237 configureActionButton(pinButton);
241 final void showHoverControls(
final boolean showControls) {
242 Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
243 eventType ->
new DropShadow(-10, getColor(eventType)));
244 setEffect(showControls ? dropShadow : null);
246 enableTooltip(showControls);
247 installActionButtons();
252 pinButton.setOnAction(actionEvent -> {
253 new UnPinEventAction(controller, tlEvent).handle(actionEvent);
254 showHoverControls(
true);
256 pinButton.setGraphic(
new ImageView(UNPIN));
258 pinButton.setOnAction(actionEvent -> {
259 new PinEventAction(controller, tlEvent).handle(actionEvent);
260 showHoverControls(
true);
262 pinButton.setGraphic(
new ImageView(PIN));
265 show(controlsHBox, showControls);
266 if (parentNode != null) {
267 parentNode.showHoverControls(
false);
275 @NbBundle.Messages({
"# {0} - counts",
276 "# {1} - event type",
277 "# {2} - description",
278 "# {3} - start date/time",
279 "# {4} - end date/time",
280 "EventNodeBase.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}",
281 "EventNodeBase.toolTip.loading2=loading tooltip",
282 "# {0} - hash set count string",
283 "EventNodeBase.toolTip.hashSetHits=\n\nHash Set Hits\n{0}",
284 "# {0} - tag count string",
285 "EventNodeBase.toolTip.tags=\n\nTags\n{0}"})
286 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
287 void installTooltip() {
288 if (tooltip.getText().equalsIgnoreCase(Bundle.EventBundleNodeBase_toolTip_loading())) {
289 final Task<String> tooltTipTask =
new Task<String>() {
291 updateTitle(Bundle.EventNodeBase_toolTip_loading2());
295 protected String call() throws Exception {
296 HashMap<String, Long> hashSetCounts =
new HashMap<>();
297 if (tlEvent.getEventIDsWithHashHits().isEmpty() ==
false) {
300 for (TimelineEvent tle : eventsModel.
getEventsById(tlEvent.getEventIDsWithHashHits())) {
301 Set<String> hashSetNames = sleuthkitCase.getContentById(tle.getFileObjID()).getHashSetNames();
302 for (String hashSetName : hashSetNames) {
303 hashSetCounts.merge(hashSetName, 1L, Long::sum);
306 }
catch (TskCoreException ex) {
307 LOGGER.log(Level.SEVERE,
"Error getting hashset hit info for event.", ex);
310 String hashSetCountsString = hashSetCounts.entrySet().stream()
311 .map((Map.Entry<String, Long> t) -> t.getKey() +
" : " + t.getValue())
312 .collect(Collectors.joining(
"\n"));
314 Map<String, Long> tagCounts =
new HashMap<>();
315 if (tlEvent.getEventIDsWithTags().isEmpty() ==
false) {
318 String tagCountsString = tagCounts.entrySet().stream()
319 .map((Map.Entry<String, Long> t) -> t.getKey() +
" : " + t.getValue())
320 .collect(Collectors.joining(
"\n"));
322 return Bundle.EventNodeBase_tooltip_text(getEventIDs().size(), getEventType(), getDescription(),
323 TimeLineController.getZonedFormatter().print(getStartMillis()),
324 TimeLineController.getZonedFormatter().print(getEndMillis() + 1000))
325 + (hashSetCountsString.isEmpty() ?
"" : Bundle.EventNodeBase_toolTip_hashSetHits(hashSetCountsString))
326 + (tagCountsString.isEmpty() ?
"" : Bundle.EventNodeBase_toolTip_tags(tagCountsString));
330 protected void done() {
333 tooltip.setText(
get());
334 tooltip.setGraphic(null);
335 }
catch (InterruptedException | ExecutionException ex) {
336 LOGGER.log(Level.SEVERE,
"Tooltip generation failed.", ex);
340 new Thread(tooltTipTask).start();
341 chartLane.getController().monitorTask(tooltTipTask);
345 void enableTooltip(
boolean toolTipEnabled) {
346 if (toolTipEnabled) {
347 Tooltip.install(
this, tooltip);
349 Tooltip.uninstall(
this, tooltip);
353 final TimelineEventType getEventType() {
354 return tlEvent.getEventType();
357 long getStartMillis() {
358 return tlEvent.getStartMillis();
361 final long getEndMillis() {
362 return tlEvent.getEndMillis();
365 final double getLayoutXCompensation() {
366 return parentNode != null
367 ? getChartLane().getXAxis().getDisplayPosition(
new DateTime(parentNode.getStartMillis()))
371 abstract String getDescription();
373 void animateTo(
double xLeft,
double yTop) {
374 if (timeline != null) {
376 Platform.runLater(this::requestChartLayout);
378 timeline =
new Timeline(
new KeyFrame(Duration.millis(100),
379 new KeyValue(layoutXProperty(), xLeft),
380 new KeyValue(layoutYProperty(), yTop))
382 timeline.setOnFinished(finished -> Platform.runLater(this::requestChartLayout));
386 void requestChartLayout() {
387 getChartLane().requestChartLayout();
390 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
391 void setDescriptionVisibility(DescriptionVisibility descrVis) {
392 final int size =
getEvent().getSize();
402 showFullDescription(size);
407 void showCountOnly(
final int size) {
408 descrLabel.setText(
"");
409 countLabel.setText(String.valueOf(size));
412 void hideDescription() {
413 countLabel.setText(
"");
414 descrLabel.setText(
"");
422 synchronized void applyHighlightEffect(
boolean applied) {
424 descrLabel.setStyle(
"-fx-font-weight: bold;");
425 setBackground(highlightedBackground);
427 descrLabel.setStyle(
"-fx-font-weight: normal;");
428 setBackground(defaultBackground);
432 void applyHighlightEffect() {
433 applyHighlightEffect(
true);
436 void clearHighlightEffect() {
437 applyHighlightEffect(
false);
440 abstract Collection<Long> getEventIDs();
442 abstract EventHandler<MouseEvent> getDoubleClickHandler();
444 Iterable<? extends Action> getActions() {
458 ContextMenu chartContextMenu = chartLane.getContextMenu(mouseEvent);
460 ContextMenu contextMenu = ActionUtils.createContextMenu(Lists.newArrayList(getActions()));
461 contextMenu.getItems().add(
new SeparatorMenuItem());
462 contextMenu.getItems().addAll(chartContextMenu.getItems());
463 contextMenu.setAutoHide(
true);
467 void showFullDescription(
final int size) {
468 countLabel.setText((size == 1) ?
"" :
" (" + size +
")");
470 ->
" ..." + StringUtils.substringAfter(
getEvent().getDescription(), parentNode.getDescription()))
471 .orElseGet(
getEvent()::getDescription);
473 descrLabel.setText(description);
479 Platform.runLater(() -> {
492 if (
false == difference.isEmpty()) {
493 Platform.runLater(() -> {
501 @NbBundle.Messages({
"PinEventAction.text=Pin"})
503 super(Bundle.PinEventAction_text());
504 setEventHandler(actionEvent -> controller.
pinEvent(event));
505 setGraphic(
new ImageView(PIN));
511 @NbBundle.Messages({
"UnPinEventAction.text=Unpin"})
513 super(Bundle.UnPinEventAction_text());
514 setEventHandler(actionEvent -> controller.
unPinEvent(event));
515 setGraphic(
new ImageView(UNPIN));
526 if (t.getButton() == MouseButton.PRIMARY) {
527 if (t.getClickCount() > 1) {
528 getDoubleClickHandler().handle(t);
529 }
else if (t.isShiftDown()) {
531 }
else if (t.isShortcutDown()) {
537 }
else if (t.isPopupTrigger() && t.isStillSincePress()) {
void unPinEvent(DetailViewEvent event)
static final Image HASH_HIT
Map< String, Long > getTagCountsByTagName(Set< Long > eventIDsWithTags)
Set< TimelineEvent > getEventsById(Collection< Long > eventIDs)
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)