19package org.sleuthkit.autopsy.timeline.ui;
21import com.google.common.collect.ImmutableList;
22import com.google.common.eventbus.Subscribe;
23import java.time.Instant;
24import java.time.LocalDate;
25import java.time.LocalDateTime;
26import java.time.ZoneOffset;
27import java.util.ArrayList;
29import java.util.function.BiFunction;
30import java.util.function.Supplier;
31import java.util.logging.Level;
32import javafx.application.Platform;
33import javafx.beans.InvalidationListener;
34import javafx.beans.Observable;
35import javafx.collections.FXCollections;
36import javafx.collections.ObservableList;
37import javafx.fxml.FXML;
38import javafx.geometry.Insets;
39import javafx.scene.Node;
40import javafx.scene.control.Button;
41import javafx.scene.control.Label;
42import javafx.scene.control.MenuButton;
43import javafx.scene.control.TitledPane;
44import javafx.scene.control.ToggleButton;
45import javafx.scene.control.ToolBar;
46import javafx.scene.control.Tooltip;
47import javafx.scene.effect.Lighting;
48import javafx.scene.image.Image;
49import javafx.scene.image.ImageView;
50import javafx.scene.input.MouseEvent;
51import javafx.scene.layout.Background;
52import javafx.scene.layout.BackgroundFill;
53import javafx.scene.layout.BorderPane;
54import javafx.scene.layout.CornerRadii;
55import javafx.scene.layout.HBox;
56import javafx.scene.layout.Priority;
57import javafx.scene.layout.Region;
58import static javafx.scene.layout.Region.USE_PREF_SIZE;
59import javafx.scene.layout.StackPane;
60import javafx.scene.paint.Color;
61import javafx.util.Callback;
62import javax.annotation.Nonnull;
63import javax.annotation.concurrent.GuardedBy;
64import jfxtras.scene.control.LocalDateTimePicker;
65import jfxtras.scene.control.LocalDateTimeTextField;
66import jfxtras.scene.control.ToggleGroupValue;
67import org.controlsfx.control.NotificationPane;
68import org.controlsfx.control.Notifications;
69import org.controlsfx.control.RangeSlider;
70import org.controlsfx.control.SegmentedButton;
71import org.controlsfx.control.action.Action;
72import org.controlsfx.control.action.ActionUtils;
73import org.joda.time.DateTime;
74import org.joda.time.Interval;
75import org.openide.util.NbBundle;
76import org.sleuthkit.autopsy.coreutils.LoggedTask;
77import org.sleuthkit.autopsy.coreutils.Logger;
78import org.sleuthkit.autopsy.coreutils.ThreadConfined;
79import org.sleuthkit.autopsy.timeline.FXMLConstructor;
80import org.sleuthkit.autopsy.timeline.EventsModel;
81import org.sleuthkit.autopsy.timeline.TimeLineController;
82import org.sleuthkit.autopsy.timeline.ViewMode;
83import org.sleuthkit.autopsy.timeline.actions.AddManualEvent;
84import org.sleuthkit.autopsy.timeline.actions.Back;
85import org.sleuthkit.autopsy.timeline.actions.ResetFilters;
86import org.sleuthkit.autopsy.timeline.actions.SaveSnapshotAsReport;
87import org.sleuthkit.autopsy.timeline.actions.ZoomIn;
88import org.sleuthkit.autopsy.timeline.actions.ZoomOut;
89import org.sleuthkit.autopsy.timeline.actions.ZoomToEvents;
90import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent;
91import org.sleuthkit.autopsy.timeline.events.TagsUpdatedEvent;
92import org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane;
93import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
94import org.sleuthkit.autopsy.timeline.ui.detailview.tree.EventsTree;
95import org.sleuthkit.autopsy.timeline.ui.listvew.ListViewPane;
96import org.sleuthkit.autopsy.timeline.utils.RangeDivision;
97import org.sleuthkit.datamodel.TskCoreException;
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));
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) {
254 if (
false ==
controller.pushTimeRange(
new Interval(
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);
315 @SuppressWarnings(
"this-escape")
318 this.filteredEvents =
controller.getEventsModel();
326 "ViewFrame.viewModeLabel.text=View Mode:",
327 "ViewFrame.startLabel.text=Start:",
328 "ViewFrame.endLabel.text=End:",
329 "ViewFrame.countsToggle.text=Counts",
330 "ViewFrame.detailsToggle.text=Details",
331 "ViewFrame.listToggle.text=List",
332 "ViewFrame.zoomMenuButton.text=Zoom in/out to",
333 "ViewFrame.zoomMenuButton.errorMessage=Error pushing time range.",
334 "ViewFrame.tagsAddedOrDeleted=Tags have been created and/or deleted. The view may not be up to date."
337 assert
endPicker != null :
"fx:id=\"endPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
338 assert
histogramBox != null :
"fx:id=\"histogramBox\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
339 assert
startPicker != null :
"fx:id=\"startPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
340 assert
rangeHistogramStack != null :
"fx:id=\"rangeHistogramStack\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
341 assert
countsToggle != null :
"fx:id=\"countsToggle\" was not injected: check your FXML file 'VisToggle.fxml'.";
342 assert
detailsToggle != null :
"fx:id=\"eventsToggle\" was not injected: check your FXML file 'VisToggle.fxml'.";
354 viewModeLabel.setText(Bundle.ViewFrame_viewModeLabel_text());
355 countsToggle.setText(Bundle.ViewFrame_countsToggle_text());
356 detailsToggle.setText(Bundle.ViewFrame_detailsToggle_text());
357 listToggle.setText(Bundle.ViewFrame_listToggle_text());
374 startLabel.setText(Bundle.ViewFrame_startLabel_text());
375 endLabel.setText(Bundle.ViewFrame_endLabel_text());
379 startPicker.setParseErrorCallback(throwable ->
null);
380 endPicker.setParseErrorCallback(throwable ->
null);
383 LocalDateDisabler localDateDisabler =
new LocalDateDisabler();
384 startPicker.setLocalDateTimeRangeCallback(localDateDisabler);
385 endPicker.setLocalDateTimeRangeCallback(localDateDisabler);
402 histogramBox.setStyle(
" -fx-padding: 0,0.5em,0,.5em; ");
406 for (ZoomRanges zoomRange : ZoomRanges.values()) {
408 new Action(zoomRange.getDisplayName(), actionEvent -> {
410 controller.pushPeriod(zoomRange.getPeriod());
411 } catch (TskCoreException ex) {
412 Notifications.create().owner(getScene().getWindow())
413 .text(Bundle.ViewFrame_zoomMenuButton_errorMessage())
415 logger.log(Level.SEVERE,
"Error pushing a time range.", ex);
428 TimeLineController.timeZoneProperty().addListener(timeZoneProp ->
refreshTimeUI());
446 Platform.runLater(() -> {
463 Platform.runLater(() -> {
477 "ViewFrame.notification.cacheInvalidated=The event data has been updated, the visualization may be out of date."})
479 Platform.runLater(() -> {
490 @NbBundle.Messages({
"ViewFrame.histogramTask.title=Rebuilding Histogram",
491 "ViewFrame.histogramTask.preparing=Preparing",
492 "ViewFrame.histogramTask.resetUI=Resetting UI",
493 "ViewFrame.histogramTask.queryDb=Querying FB",
494 "ViewFrame.histogramTask.updateUI2=Updating UI"})
501 private final Lighting lighting =
new Lighting();
504 protected Void call()
throws Exception {
506 updateMessage(Bundle.ViewFrame_histogramTask_preparing());
518 Platform.runLater(() -> {
519 updateMessage(Bundle.ViewFrame_histogramTask_resetUI());
523 ArrayList<Long> bins =
new ArrayList<>();
525 DateTime start = timeRange.getStart();
526 while (timeRange.contains(start)) {
531 final Interval interval =
new Interval(start, end);
536 updateMessage(Bundle.ViewFrame_histogramTask_queryDb());
538 long count =
filteredEvents.getEventCounts(interval).values().stream().mapToLong(Long::valueOf).sum();
541 max = Math.max(count, max);
543 final double fMax = Math.log(max);
544 final ArrayList<Long> fbins =
new ArrayList<>(bins);
545 Platform.runLater(() -> {
546 updateMessage(Bundle.ViewFrame_histogramTask_updateUI2());
550 for (Long bin : fbins) {
554 Region bar =
new Region();
556 bar.prefHeightProperty().bind(
histogramBox.heightProperty().multiply(Math.log(bin)).divide(fMax));
557 bar.setMaxHeight(USE_PREF_SIZE);
558 bar.setMinHeight(USE_PREF_SIZE);
560 bar.setOnMouseEntered((MouseEvent event) -> {
561 Tooltip.install(bar,
new Tooltip(bin.toString()));
563 bar.setEffect(lighting);
565 HBox.setHgrow(bar, Priority.ALWAYS);
582 "ViewFrame.refreshTimeUI.errorMessage=Error gettig the spanning interval."})
589 long startMillis =
filteredEvents.getTimeRange().getStartMillis();
592 if ( maxTime > minTime) {
593 Platform.runLater(() -> {
612 }
catch (TskCoreException ex) {
613 Notifications.create().owner(getScene().getWindow())
614 .text(Bundle.ViewFrame_refreshTimeUI_errorMessage())
616 logger.log(Level.SEVERE,
"Error gettig the spanning interval.", ex);
634 switch (newViewMode) {
651 throw new IllegalArgumentException(
"Unknown ViewMode: " + newViewMode.toString());
655 controller.getAutopsyCase().getSleuthkitCase().registerForEvents(
this);
671 hostedView.hasVisibleEventsProperty().addListener(hasEvents -> {
709 @NbBundle.Messages(
"NoEventsDialog.titledPane.text=No Visible Events")
727 @SuppressWarnings(
"this-escape")
734 @NbBundle.Messages(
"ViewFrame.noEventsDialogLabel.text=There are no events visible with the current zoom / filter settings.")
736 assert resetFiltersButton != null :
"fx:id=\"resetFiltersButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'.";
737 assert dismissButton != null :
"fx:id=\"dismissButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'.";
738 assert zoomButton != null :
"fx:id=\"zoomButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'.";
740 titledPane.setText(Bundle.NoEventsDialog_titledPane_text());
741 noEventsDialogLabel.setText(Bundle.ViewFrame_noEventsDialogLabel_text());
743 dismissButton.setOnAction(actionEvent -> closeCallback.run());
755 private class PickerListener
implements InvalidationListener {
765 @NbBundle.Messages({
"ViewFrame.pickerListener.errorMessage=Error responding to date/time picker change."})
768 LocalDateTime pickerTime =
pickerSupplier.get().getLocalDateTime();
769 if (pickerTime !=
null) {
772 }
catch (TskCoreException ex) {
773 Notifications.create().owner(getScene().getWindow())
774 .text(Bundle.ViewFrame_pickerListener_errorMessage())
776 logger.log(Level.WARNING,
"Error responding to date/time picker change.", ex);
777 }
catch (IllegalArgumentException ex ) {
778 logger.log(Level.INFO,
"Timeline: User supplied invalid time range.");
781 Platform.runLater(
ViewFrame.this::refreshTimeUI);
789 private class LocalDateDisabler implements Callback<LocalDateTimePicker.LocalDateTimeRange, Void> {
792 "ViewFrame.localDateDisabler.errorMessage=Error getting spanning interval."})
794 public Void
call(LocalDateTimePicker.LocalDateTimeRange viewedRange) {
797 endPicker.disabledLocalDateTimes().clear();
801 long spanStartMillis = spanningInterval.getStartMillis();
802 long spaneEndMillis = spanningInterval.getEndMillis();
804 LocalDate rangeStartLocalDate = viewedRange.getStartLocalDateTime().toLocalDate();
805 LocalDate rangeEndLocalDate = viewedRange.getEndLocalDateTime().toLocalDate().plusDays(1);
807 for (LocalDate dt = rangeStartLocalDate;
false == dt.isAfter(rangeEndLocalDate); dt = dt.plusDays(1)) {
808 long startOfDay = dt.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
809 long endOfDay = dt.plusDays(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
811 if (endOfDay < spanStartMillis || startOfDay > spaneEndMillis) {
812 startPicker.disabledLocalDateTimes().add(dt.atStartOfDay());
813 endPicker.disabledLocalDateTimes().add(dt.atStartOfDay());
817 }
catch (TskCoreException ex) {
818 Notifications.create().owner(getScene().getWindow())
819 .text(Bundle.ViewFrame_localDateDisabler_errorMessage())
821 logger.log(Level.SEVERE,
"Error getting spanning interval.", ex);
832 private class LocalDateTimeValidator
implements Callback<LocalDateTime, Boolean> {
837 private final LocalDateTimeTextField
picker;
839 LocalDateTimeValidator(LocalDateTimeTextField
picker) {
844 "ViewFrame.dateTimeValidator.errorMessage=Error getting spanning interval."})
846 public Boolean
call(LocalDateTime param) {
852 if (
picker.isPickerShowing() ==
false) {
854 picker.setDisplayedLocalDateTime(
picker.getLocalDateTime());
858 }
catch (TskCoreException ex) {
859 Notifications.create().owner(getScene().getWindow())
860 .text(Bundle.ViewFrame_dateTimeValidator_errorMessage())
862 logger.log(Level.SEVERE,
"Error getting spanning interval.", ex);
871 private class Refresh
extends Action {
874 "ViewFrame.refresh.text=Refresh View",
875 "ViewFrame.refresh.longText=Refresh the view to include information that is in the DB but not displayed, such as newly updated tags."})
877 super(Bundle.ViewFrame_refresh_text());
878 setLongText(Bundle.ViewFrame_refresh_longText());
879 setGraphic(
new ImageView(
REFRESH));
880 setEventHandler(actionEvent ->
filteredEvents.postRefreshRequest());
881 disabledProperty().bind(
hostedView.needsRefreshProperty().not());
synchronized static Logger getLogger(String name)
synchronized void registerForEvents(Object subscriber)
final ReadOnlyObjectWrapper< Interval > timeRangeProperty
final ReadOnlyObjectWrapper< EventsModelParams > modelParamsProperty
static void construct(Node node, String fxmlFileName)
static DateTimeZone getJodaTimeZone()
synchronized ReadOnlyObjectProperty< ViewMode > viewModeProperty()
static ZoneId getTimeZoneID()
synchronized void setViewMode(ViewMode viewMode)
Void call(LocalDateTimePicker.LocalDateTimeRange viewedRange)
Boolean call(LocalDateTime param)
final LocalDateTimeTextField picker
final Runnable closeCallback
Label noEventsDialogLabel
NoEventsDialog(Runnable closeCallback)
Button resetFiltersButton
final BiFunction< Interval, Long, Interval > intervalMapper
void invalidated(Observable observable)
final Supplier< LocalDateTimeTextField > pickerSupplier
void handleTimeLineTagUpdate(TagsUpdatedEvent event)
final ObservableList< Node > timeNavigationNodes
final InvalidationListener zoomListener
final NotificationPane notificationPane
ViewFrame(@Nonnull TimeLineController controller, @Nonnull EventsTree eventsTree)
void setViewSettingsControls(List< Node > newSettingsNodes)
final EventsModel filteredEvents
MenuButton zoomMenuButton
void setTimeNavigationControls(List< Node > timeNavigationNodes)
final InvalidationListener endListener
AbstractTimeLineView hostedView
ToggleButton detailsToggle
final TimeLineController controller
static final int TIME_TOOLBAR_INSERTION_INDEX
LocalDateTimeTextField startPicker
static final Image WARNING
static final Background GRAY_BACKGROUND
LoggedTask< Void > histogramTask
ToggleGroupValue< ViewMode > viewModeToggleGroup
static long localDateTimeToEpochMilli(LocalDateTime localDateTime)
static final Image REFRESH
static final int SETTINGS_TOOLBAR_INSERTION_INDEX
StackPane rangeHistogramStack
ToggleButton countsToggle
static final Logger logger
void handleCacheInvalidated(EventsModel.CacheInvalidatedEvent event)
static LocalDateTime epochMillisToLocalDateTime(long millis)
final InvalidationListener startListener
final InvalidationListener rangeSliderListener
final ObservableList< Node > settingsNodes
void handleRefreshRequested(RefreshRequestedEvent event)
static final Region NO_EVENTS_BACKGROUND
ImmutableList< Node > defaultTimeNavigationNodes
synchronized void refreshHistorgram()
LocalDateTimeTextField endPicker
final RangeSlider rangeSlider
SegmentedButton modeSegButton
final EventsTree eventsTree
void setHighLightedEvents(ObservableList< DetailViewEvent > highlightedEvents)
TimeUnits getPeriodSize()
static RangeDivision getRangeDivision(Interval timeRange, DateTimeZone timeZone)