19 package org.sleuthkit.autopsy.timeline.ui;
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.eventbus.Subscribe;
23 import java.time.Instant;
24 import java.time.LocalDate;
25 import java.time.LocalDateTime;
26 import java.time.ZoneOffset;
27 import java.util.ArrayList;
28 import java.util.List;
29 import java.util.function.BiFunction;
30 import java.util.function.Supplier;
31 import java.util.logging.Level;
32 import javafx.application.Platform;
33 import javafx.beans.InvalidationListener;
34 import javafx.beans.Observable;
35 import javafx.collections.FXCollections;
36 import javafx.collections.ObservableList;
37 import javafx.fxml.FXML;
38 import javafx.geometry.Insets;
39 import javafx.scene.Node;
40 import javafx.scene.control.Button;
41 import javafx.scene.control.Label;
42 import javafx.scene.control.MenuButton;
43 import javafx.scene.control.TitledPane;
44 import javafx.scene.control.ToggleButton;
45 import javafx.scene.control.ToolBar;
46 import javafx.scene.control.Tooltip;
47 import javafx.scene.effect.Lighting;
48 import javafx.scene.image.Image;
49 import javafx.scene.image.ImageView;
50 import javafx.scene.input.MouseEvent;
51 import javafx.scene.layout.Background;
52 import javafx.scene.layout.BackgroundFill;
53 import javafx.scene.layout.BorderPane;
54 import javafx.scene.layout.CornerRadii;
55 import javafx.scene.layout.HBox;
56 import javafx.scene.layout.Priority;
57 import javafx.scene.layout.Region;
58 import static javafx.scene.layout.Region.USE_PREF_SIZE;
59 import javafx.scene.layout.StackPane;
60 import javafx.scene.paint.Color;
61 import javafx.util.Callback;
62 import javax.annotation.Nonnull;
63 import javax.annotation.concurrent.GuardedBy;
64 import jfxtras.scene.control.LocalDateTimePicker;
65 import jfxtras.scene.control.LocalDateTimeTextField;
66 import jfxtras.scene.control.ToggleGroupValue;
67 import org.controlsfx.control.NotificationPane;
68 import org.controlsfx.control.Notifications;
69 import org.controlsfx.control.RangeSlider;
70 import org.controlsfx.control.SegmentedButton;
71 import org.controlsfx.control.action.Action;
72 import org.controlsfx.control.action.ActionUtils;
73 import org.joda.time.DateTime;
74 import org.joda.time.Interval;
75 import org.openide.util.NbBundle;
111 private static final Image
WARNING =
new Image(
"org/sleuthkit/autopsy/timeline/images/warning_triangle.png", 16, 16,
true,
true);
112 private static final Image
REFRESH =
new Image(
"org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png");
113 private static final Background
GRAY_BACKGROUND =
new Background(
new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY));
122 setBackground(GRAY_BACKGROUND);
159 private final RangeSlider
rangeSlider = new RangeSlider(0, 1.0, .25, .75);
224 private final ObservableList<Node>
settingsNodes = FXCollections.observableArrayList();
246 "ViewFrame.rangeSliderListener.errorMessage=Error responding to range slider."})
249 public void invalidated(Observable observable) {
256 (
long) (
rangeSlider.getHighValue() + minTime + 1000)))) {
259 }
catch (TskCoreException ex) {
260 Notifications.create().owner(getScene().getWindow())
261 .text(Bundle.ViewFrame_rangeSliderListener_errorMessage())
263 logger.log(Level.SEVERE,
"Error responding to range slider.", ex);
325 "ViewFrame.viewModeLabel.text=View Mode:",
326 "ViewFrame.startLabel.text=Start:",
327 "ViewFrame.endLabel.text=End:",
328 "ViewFrame.countsToggle.text=Counts",
329 "ViewFrame.detailsToggle.text=Details",
330 "ViewFrame.listToggle.text=List",
331 "ViewFrame.zoomMenuButton.text=Zoom in/out to",
332 "ViewFrame.zoomMenuButton.errorMessage=Error pushing time range.",
333 "ViewFrame.tagsAddedOrDeleted=Tags have been created and/or deleted. The view may not be up to date."
336 assert
endPicker != null :
"fx:id=\"endPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
337 assert
histogramBox != null :
"fx:id=\"histogramBox\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
338 assert
startPicker != null :
"fx:id=\"startPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
339 assert
rangeHistogramStack != null :
"fx:id=\"rangeHistogramStack\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
340 assert
countsToggle != null :
"fx:id=\"countsToggle\" was not injected: check your FXML file 'VisToggle.fxml'.";
341 assert
detailsToggle != null :
"fx:id=\"eventsToggle\" was not injected: check your FXML file 'VisToggle.fxml'.";
353 viewModeLabel.setText(Bundle.ViewFrame_viewModeLabel_text());
354 countsToggle.setText(Bundle.ViewFrame_countsToggle_text());
355 detailsToggle.setText(Bundle.ViewFrame_detailsToggle_text());
356 listToggle.setText(Bundle.ViewFrame_listToggle_text());
373 startLabel.setText(Bundle.ViewFrame_startLabel_text());
374 endLabel.setText(Bundle.ViewFrame_endLabel_text());
378 startPicker.setParseErrorCallback(throwable -> null);
379 endPicker.setParseErrorCallback(throwable -> null);
382 LocalDateDisabler localDateDisabler =
new LocalDateDisabler();
383 startPicker.setLocalDateTimeRangeCallback(localDateDisabler);
384 endPicker.setLocalDateTimeRangeCallback(localDateDisabler);
401 histogramBox.setStyle(
" -fx-padding: 0,0.5em,0,.5em; ");
405 for (ZoomRanges zoomRange : ZoomRanges.values()) {
407 new Action(zoomRange.getDisplayName(), actionEvent -> {
410 }
catch (TskCoreException ex) {
411 Notifications.create().owner(getScene().getWindow())
412 .text(Bundle.ViewFrame_zoomMenuButton_errorMessage())
414 logger.log(Level.SEVERE,
"Error pushing a time range.", ex);
427 TimeLineController.timeZoneProperty().addListener(timeZoneProp ->
refreshTimeUI());
445 Platform.runLater(() -> {
462 Platform.runLater(() -> {
476 "ViewFrame.notification.cacheInvalidated=The event data has been updated, the visualization may be out of date."})
478 Platform.runLater(() -> {
489 @NbBundle.Messages({
"ViewFrame.histogramTask.title=Rebuilding Histogram",
490 "ViewFrame.histogramTask.preparing=Preparing",
491 "ViewFrame.histogramTask.resetUI=Resetting UI",
492 "ViewFrame.histogramTask.queryDb=Querying FB",
493 "ViewFrame.histogramTask.updateUI2=Updating UI"})
500 private final Lighting lighting =
new Lighting();
503 protected Void call()
throws Exception {
505 updateMessage(Bundle.ViewFrame_histogramTask_preparing());
517 Platform.runLater(() -> {
518 updateMessage(Bundle.ViewFrame_histogramTask_resetUI());
522 ArrayList<Long> bins =
new ArrayList<>();
524 DateTime start = timeRange.getStart();
525 while (timeRange.contains(start)) {
530 final Interval interval =
new Interval(start, end);
535 updateMessage(Bundle.ViewFrame_histogramTask_queryDb());
540 max = Math.max(count, max);
542 final double fMax = Math.log(max);
543 final ArrayList<Long> fbins =
new ArrayList<>(bins);
544 Platform.runLater(() -> {
545 updateMessage(Bundle.ViewFrame_histogramTask_updateUI2());
549 for (Long bin : fbins) {
553 Region bar =
new Region();
555 bar.prefHeightProperty().bind(
histogramBox.heightProperty().multiply(Math.log(bin)).divide(fMax));
556 bar.setMaxHeight(USE_PREF_SIZE);
557 bar.setMinHeight(USE_PREF_SIZE);
558 bar.setBackground(GRAY_BACKGROUND);
559 bar.setOnMouseEntered((MouseEvent event) -> {
560 Tooltip.install(bar,
new Tooltip(bin.toString()));
562 bar.setEffect(lighting);
564 HBox.setHgrow(bar, Priority.ALWAYS);
581 "ViewFrame.refreshTimeUI.errorMessage=Error gettig the spanning interval."})
591 if ( maxTime > minTime) {
592 Platform.runLater(() -> {
593 startPicker.localDateTimeProperty().removeListener(startListener);
594 endPicker.localDateTimeProperty().removeListener(endListener);
607 startPicker.localDateTimeProperty().addListener(startListener);
608 endPicker.localDateTimeProperty().addListener(endListener);
611 }
catch (TskCoreException ex) {
612 Notifications.create().owner(getScene().getWindow())
613 .text(Bundle.ViewFrame_refreshTimeUI_errorMessage())
615 logger.log(Level.SEVERE,
"Error gettig the spanning interval.", ex);
633 switch (newViewMode) {
650 throw new IllegalArgumentException(
"Unknown ViewMode: " + newViewMode.toString());
669 hostedView.hasVisibleEventsProperty().addListener(hasEvents -> {
673 NO_EVENTS_BACKGROUND,
702 this.timeNavigationNodes.setAll(timeNavigationNodes);
703 timeRangeToolBar.getItems().addAll(TIME_TOOLBAR_INSERTION_INDEX, timeNavigationNodes);
707 @NbBundle.Messages(
"NoEventsDialog.titledPane.text=No Visible Events")
731 @NbBundle.Messages(
"ViewFrame.noEventsDialogLabel.text=There are no events visible with the current zoom / filter settings.")
733 assert resetFiltersButton != null :
"fx:id=\"resetFiltersButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'.";
734 assert dismissButton != null :
"fx:id=\"dismissButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'.";
735 assert zoomButton != null :
"fx:id=\"zoomButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'.";
737 titledPane.setText(Bundle.NoEventsDialog_titledPane_text());
738 noEventsDialogLabel.setText(Bundle.ViewFrame_noEventsDialogLabel_text());
740 dismissButton.setOnAction(actionEvent -> closeCallback.run());
757 PickerListener(Supplier<LocalDateTimeTextField> pickerSupplier, BiFunction<Interval, Long, Interval> intervalMapper) {
762 @NbBundle.Messages({
"ViewFrame.pickerListener.errorMessage=Error responding to date/time picker change."})
765 LocalDateTime pickerTime = pickerSupplier.get().getLocalDateTime();
766 if (pickerTime != null) {
769 }
catch (TskCoreException ex) {
770 Notifications.create().owner(getScene().getWindow())
771 .text(Bundle.ViewFrame_pickerListener_errorMessage())
773 logger.log(Level.WARNING,
"Error responding to date/time picker change.", ex);
774 }
catch (IllegalArgumentException ex ) {
775 logger.log(Level.INFO,
"Timeline: User supplied invalid time range.");
778 Platform.runLater(
ViewFrame.this::refreshTimeUI);
786 private class LocalDateDisabler implements Callback<LocalDateTimePicker.LocalDateTimeRange, Void> {
789 "ViewFrame.localDateDisabler.errorMessage=Error getting spanning interval."})
791 public Void
call(LocalDateTimePicker.LocalDateTimeRange viewedRange) {
794 endPicker.disabledLocalDateTimes().clear();
798 long spanStartMillis = spanningInterval.getStartMillis();
799 long spaneEndMillis = spanningInterval.getEndMillis();
801 LocalDate rangeStartLocalDate = viewedRange.getStartLocalDateTime().toLocalDate();
802 LocalDate rangeEndLocalDate = viewedRange.getEndLocalDateTime().toLocalDate().plusDays(1);
804 for (LocalDate dt = rangeStartLocalDate;
false == dt.isAfter(rangeEndLocalDate); dt = dt.plusDays(1)) {
805 long startOfDay = dt.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
806 long endOfDay = dt.plusDays(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
808 if (endOfDay < spanStartMillis || startOfDay > spaneEndMillis) {
809 startPicker.disabledLocalDateTimes().add(dt.atStartOfDay());
810 endPicker.disabledLocalDateTimes().add(dt.atStartOfDay());
814 }
catch (TskCoreException ex) {
815 Notifications.create().owner(getScene().getWindow())
816 .text(Bundle.ViewFrame_localDateDisabler_errorMessage())
818 logger.log(Level.SEVERE,
"Error getting spanning interval.", ex);
834 private final LocalDateTimeTextField
picker;
841 "ViewFrame.dateTimeValidator.errorMessage=Error getting spanning interval."})
843 public Boolean
call(LocalDateTime param) {
849 if (picker.isPickerShowing() ==
false) {
851 picker.setDisplayedLocalDateTime(picker.getLocalDateTime());
855 }
catch (TskCoreException ex) {
856 Notifications.create().owner(getScene().getWindow())
857 .text(Bundle.ViewFrame_dateTimeValidator_errorMessage())
859 logger.log(Level.SEVERE,
"Error getting spanning interval.", ex);
871 "ViewFrame.refresh.text=Refresh View",
872 "ViewFrame.refresh.longText=Refresh the view to include information that is in the DB but not displayed, such as newly updated tags."})
874 super(Bundle.ViewFrame_refresh_text());
875 setLongText(Bundle.ViewFrame_refresh_longText());
876 setGraphic(
new ImageView(REFRESH));
static final Image WARNING
final EventsTree eventsTree
final Runnable closeCallback
Void call(LocalDateTimePicker.LocalDateTimeRange viewedRange)
void setHighLightedEvents(ObservableList< DetailViewEvent > highlightedEvents)
ReadOnlyBooleanProperty needsRefreshProperty()
abstract boolean hasCustomTimeNavigationControls()
final InvalidationListener zoomListener
void setViewSettingsControls(List< Node > newSettingsNodes)
synchronized ViewMode getViewMode()
void handleCacheInvalidated(EventsModel.CacheInvalidatedEvent event)
final NotificationPane notificationPane
abstract ImmutableList< Node > getTimeNavigationControls()
synchronized void refreshHistorgram()
static final Logger logger
final ObservableList< Node > settingsNodes
synchronized void setViewMode(ViewMode viewMode)
final synchronized void refresh()
ToggleButton countsToggle
Boolean call(LocalDateTime param)
LoggedTask< Void > histogramTask
final ObservableList< Node > timeNavigationNodes
abstract ImmutableList< Node > getSettingsControls()
final ReadOnlyObjectWrapper< Interval > timeRangeProperty
void handleTimeLineTagUpdate(TagsUpdatedEvent event)
synchronized boolean pushTimeRange(Interval timeRange)
void setTimeNavigationControls(List< Node > timeNavigationNodes)
final ReadOnlyBooleanWrapper hasVisibleEvents
final RangeSlider rangeSlider
Interval getSpanningInterval(DateTimeZone timeZone)
final TimeLineController controller
final InvalidationListener endListener
static final Background GRAY_BACKGROUND
static final Region NO_EVENTS_BACKGROUND
final InvalidationListener rangeSliderListener
static ZoneId getTimeZoneID()
synchronized void registerForEvents(Object listener)
void invalidated(Observable observable)
final ReadOnlyObjectWrapper< EventsModelParams > modelParamsProperty
static final int SETTINGS_TOOLBAR_INSERTION_INDEX
synchronized void monitorTask(final Task<?> task)
synchronized void pushPeriod(ReadablePeriod period)
StackPane rangeHistogramStack
ObservableList< DetailViewEvent > getSelectedEvents()
SleuthkitCase getSleuthkitCase()
final InvalidationListener startListener
ViewFrame(@Nonnull TimeLineController controller,@Nonnull EventsTree eventsTree)
static LocalDateTime epochMillisToLocalDateTime(long millis)
NoEventsDialog(Runnable closeCallback)
Label noEventsDialogLabel
void handleRefreshRequested(RefreshRequestedEvent event)
ToggleGroupValue< ViewMode > viewModeToggleGroup
final BiFunction< Interval, Long, Interval > intervalMapper
static DateTimeZone getJodaTimeZone()
LocalDateTimeTextField startPicker
static final int TIME_TOOLBAR_INSERTION_INDEX
synchronized void registerForEvents(Object subscriber)
AbstractTimeLineView hostedView
Button resetFiltersButton
TimeUnits getPeriodSize()
synchronized ReadOnlyObjectProperty< ViewMode > viewModeProperty()
synchronized static Logger getLogger(String name)
void setDetailViewPane(DetailViewPane detailViewPane)
void postRefreshRequest()
static final Image REFRESH
LocalDateTimeTextField endPicker
final Supplier< LocalDateTimeTextField > pickerSupplier
static void construct(Node node, String fxmlFileName)
ToggleButton detailsToggle
ImmutableList< Node > defaultTimeNavigationNodes
final ReadOnlyBooleanWrapper needsRefresh
synchronized Interval getTimeRange()
static RangeDivision getRangeDivision(Interval timeRange, DateTimeZone timeZone)
SegmentedButton modeSegButton
MenuButton zoomMenuButton
Map< TimelineEventType, Long > getEventCounts(Interval timeRange)
final EventsModel filteredEvents
final LocalDateTimeTextField picker
static long localDateTimeToEpochMilli(LocalDateTime localDateTime)