/* * Autopsy Forensic Browser * * Copyright 2011-16 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.timeline.ui; import com.google.common.collect.ImmutableList; import com.google.common.eventbus.Subscribe; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; import java.util.function.Supplier; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.MenuButton; import javafx.scene.control.TitledPane; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToolBar; import javafx.scene.control.Tooltip; import javafx.scene.effect.Lighting; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.BorderPane; import javafx.scene.layout.CornerRadii; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import static javafx.scene.layout.Region.USE_PREF_SIZE; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.util.Callback; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import jfxtras.scene.control.LocalDateTimePicker; import jfxtras.scene.control.LocalDateTimeTextField; import jfxtras.scene.control.ToggleGroupValue; import org.controlsfx.control.NotificationPane; import org.controlsfx.control.RangeSlider; import org.controlsfx.control.SegmentedButton; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionUtils; import org.joda.time.DateTime; import org.joda.time.Interval; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent; import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.ViewMode; import org.sleuthkit.autopsy.timeline.actions.Back; import org.sleuthkit.autopsy.timeline.actions.ResetFilters; import org.sleuthkit.autopsy.timeline.actions.SaveSnapshotAsReport; import org.sleuthkit.autopsy.timeline.actions.UpdateDB; import org.sleuthkit.autopsy.timeline.actions.ZoomIn; import org.sleuthkit.autopsy.timeline.actions.ZoomOut; import org.sleuthkit.autopsy.timeline.actions.ZoomToEvents; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.events.DBUpdatedEvent; import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; import org.sleuthkit.autopsy.timeline.events.TagsUpdatedEvent; import static org.sleuthkit.autopsy.timeline.ui.Bundle.*; import org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; import org.sleuthkit.autopsy.timeline.ui.detailview.tree.EventsTree; import org.sleuthkit.autopsy.timeline.ui.listvew.ListViewPane; import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; /** * A container for an AbstractTimelineView. Has a Toolbar on top to hold * settings widgets supplied by contained AbstractTimelineView, and the * histogram / time selection on bottom. The time selection Toolbar has default * controls that can be replaced by ones supplied by the current view. * * TODO: Refactor common code out of histogram and CountsView? -jm */ final public class ViewFrame extends BorderPane { private static final Logger LOGGER = Logger.getLogger(ViewFrame.class.getName()); private static final Image INFORMATION = new Image("org/sleuthkit/autopsy/timeline/images/information.png", 16, 16, true, true); //NON-NLS private static final Image WARNING = new Image("org/sleuthkit/autopsy/timeline/images/warning_triangle.png", 16, 16, true, true); //NON-NLS private static final Image REFRESH = new Image("org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png"); //NON-NLS private static final Background GRAY_BACKGROUND = new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)); /** * Region that will be stacked in between the no-events "dialog" and the * hosted AbstractTimelineView in order to gray out the * AbstractTimelineView. */ private final static Region NO_EVENTS_BACKGROUND = new Region() { { setBackground(GRAY_BACKGROUND); setOpacity(.3); } }; /** * The scene graph Nodes for the current view's settings will be inserted * into the toolbar at this index. */ private static final int SETTINGS_TOOLBAR_INSERTION_INDEX = 2; /** * The scene graph Nodes for the current view's time navigation controls * will be inserted into the toolbar at this index. */ private static final int TIME_TOOLBAR_INSERTION_INDEX = 2; @GuardedBy("this") private LoggedTask<Void> histogramTask; private final EventsTree eventsTree; private AbstractTimeLineView hostedView; /* * HBox that contains the histogram bars. * * //TODO: Abstract this into a seperate class, and/or use a real bar * chart? -jm */ @FXML private HBox histogramBox; /* * Stack pane that superimposes rangeslider over histogram */ @FXML private StackPane rangeHistogramStack; private final RangeSlider rangeSlider = new RangeSlider(0, 1.0, .25, .75); /** * The lower tool bar that has controls to adjust the viewed timerange. */ @FXML private ToolBar timeRangeToolBar; /** * Parent for the default zoom in/out buttons that can be replaced in some * views(eg List View) */ @FXML private HBox zoomInOutHBox; //// time range selection components @FXML private MenuButton zoomMenuButton; @FXML private Button zoomOutButton; @FXML private Button zoomInButton; @FXML private LocalDateTimeTextField startPicker; @FXML private LocalDateTimeTextField endPicker; @FXML private Label startLabel; @FXML private Label endLabel; //// header toolbar componenets @FXML private ToolBar toolBar; private ToggleGroupValue<ViewMode> viewModeToggleGroup; @FXML private Label viewModeLabel; @FXML private SegmentedButton modeSegButton; @FXML private ToggleButton countsToggle; @FXML private ToggleButton detailsToggle; @FXML private ToggleButton listToggle; @FXML private Button snapShotButton; @FXML private Button refreshButton; @FXML private Button updateDBButton; /* * Default zoom in/out buttons provided by the ViewFrame, some views replace * these with other nodes (eg, list view) */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private ImmutableList<Node> defaultTimeNavigationNodes; /* * The settings nodes for the current view. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private final ObservableList<Node> settingsNodes = FXCollections.observableArrayList(); /* * The time nagivation nodes for the current view. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private final ObservableList<Node> timeNavigationNodes = FXCollections.observableArrayList(); /** * Wraps the contained AbstractTimelineView so that we can show * notifications over it. */ private final NotificationPane notificationPane = new NotificationPane(); private final TimeLineController controller; private final FilteredEventsModel filteredEvents; /** * Listen to changes in the range slider selection and forward to the * controller. Waits until the user releases thumb to send to controller. */ private final InvalidationListener rangeSliderListener = new InvalidationListener() { @Override public void invalidated(Observable observable) { if (rangeSlider.isHighValueChanging() == false && rangeSlider.isLowValueChanging() == false) { Long minTime = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()).getLowerBound(); if (false == controller.pushTimeRange(new Interval( (long) (rangeSlider.getLowValue() + minTime), (long) (rangeSlider.getHighValue() + minTime + 1000)))) { refreshTimeUI(); } } } }; /** * hides the notification pane on any event */ private final InvalidationListener zoomListener = any -> handleRefreshRequested(null); /** * listen to change in end time picker and push to controller */ private final InvalidationListener endListener = new PickerListener(() -> endPicker, Interval::withEndMillis); /** * listen to change in start time picker and push to controller */ private final InvalidationListener startListener = new PickerListener(() -> startPicker, Interval::withStartMillis); /** * Convert the given LocalDateTime to epoch millis USING THE CURRENT * TIMEZONE FROM THE TIMELINECONTROLLER * * @param localDateTime The LocalDateTime to convert to millis since the * Unix epoch. * * @return the given LocalDateTime as epoch millis */ private static long localDateTimeToEpochMilli(LocalDateTime localDateTime) { return localDateTime.atZone(TimeLineController.getTimeZoneID()).toInstant().toEpochMilli(); } /** * Convert the given "millis from the Unix Epoch" to a LocalDateTime USING * THE CURRENT TIMEZONE FROM THE TIMELINECONTROLLER * * @param millis The milliseconds to convert. * * @return The given epoch millis as a LocalDateTime */ private static LocalDateTime epochMillisToLocalDateTime(long millis) { return LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), TimeLineController.getTimeZoneID()); } /** * Constructor * * @param controller The TimeLineController for this ViewFrame * @param eventsTree The EventsTree this ViewFrame hosts. */ public ViewFrame(@Nonnull TimeLineController controller, @Nonnull EventsTree eventsTree) { this.controller = controller; this.filteredEvents = controller.getEventsModel(); this.eventsTree = eventsTree; FXMLConstructor.construct(this, "ViewFrame.fxml"); //NON-NLS } @FXML @NbBundle.Messages({ "ViewFrame.viewModeLabel.text=View Mode:", "ViewFrame.startLabel.text=Start:", "ViewFrame.endLabel.text=End:", "ViewFrame.countsToggle.text=Counts", "ViewFrame.detailsToggle.text=Details", "ViewFrame.listToggle.text=List", "ViewFrame.zoomMenuButton.text=Zoom in/out to", "ViewFrame.tagsAddedOrDeleted=Tags have been created and/or deleted. The view may not be up to date." }) void initialize() { assert endPicker != null : "fx:id=\"endPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'."; //NON-NLS assert histogramBox != null : "fx:id=\"histogramBox\" was not injected: check your FXML file 'ViewWrapper.fxml'."; //NON-NLS assert startPicker != null : "fx:id=\"startPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'."; //NON-NLS assert rangeHistogramStack != null : "fx:id=\"rangeHistogramStack\" was not injected: check your FXML file 'ViewWrapper.fxml'."; //NON-NLS assert countsToggle != null : "fx:id=\"countsToggle\" was not injected: check your FXML file 'VisToggle.fxml'."; //NON-NLS assert detailsToggle != null : "fx:id=\"eventsToggle\" was not injected: check your FXML file 'VisToggle.fxml'."; //NON-NLS defaultTimeNavigationNodes = ImmutableList.of(zoomInOutHBox, zoomMenuButton); timeNavigationNodes.setAll(defaultTimeNavigationNodes); //configure notification pane notificationPane.getStyleClass().add(NotificationPane.STYLE_CLASS_DARK); setCenter(notificationPane); //configure view mode toggle viewModeLabel.setText(Bundle.ViewFrame_viewModeLabel_text()); countsToggle.setText(Bundle.ViewFrame_countsToggle_text()); detailsToggle.setText(Bundle.ViewFrame_detailsToggle_text()); listToggle.setText(Bundle.ViewFrame_listToggle_text()); viewModeToggleGroup = new ToggleGroupValue<>(); viewModeToggleGroup.add(listToggle, ViewMode.LIST); viewModeToggleGroup.add(detailsToggle, ViewMode.DETAIL); viewModeToggleGroup.add(countsToggle, ViewMode.COUNTS); modeSegButton.setToggleGroup(viewModeToggleGroup); viewModeToggleGroup.valueProperty().addListener((observable, oldViewMode, newViewVode) -> controller.setViewMode(newViewVode != null ? newViewVode : (oldViewMode != null ? oldViewMode : ViewMode.COUNTS)) ); controller.viewModeProperty().addListener(viewMode -> syncViewMode()); syncViewMode(); ActionUtils.configureButton(new SaveSnapshotAsReport(controller, notificationPane::getContent), snapShotButton); ActionUtils.configureButton(new UpdateDB(controller), updateDBButton); /////configure start and end pickers startLabel.setText(Bundle.ViewFrame_startLabel_text()); endLabel.setText(Bundle.ViewFrame_endLabel_text()); //suppress stacktraces on malformed input //TODO: should we do anything else? show a warning? startPicker.setParseErrorCallback(throwable -> null); endPicker.setParseErrorCallback(throwable -> null); //disable dates outside scope of case LocalDateDisabler localDateDisabler = new LocalDateDisabler(); startPicker.setLocalDateTimeRangeCallback(localDateDisabler); endPicker.setLocalDateTimeRangeCallback(localDateDisabler); //prevent selection of (date/)times outside the scope of this case startPicker.setValueValidationCallback(new LocalDateTimeValidator(startPicker)); endPicker.setValueValidationCallback(new LocalDateTimeValidator(endPicker)); //setup rangeslider rangeSlider.setOpacity(.7); rangeSlider.setMin(0); rangeSlider.setBlockIncrement(1); rangeHistogramStack.getChildren().add(rangeSlider); /* * This padding attempts to compensates for the fact that the * rangeslider track doesn't extend to edge of node,and so the * histrogram doesn't quite line up with the rangeslider */ histogramBox.setStyle(" -fx-padding: 0,0.5em,0,.5em; "); //NON-NLS //configure zoom buttons zoomMenuButton.getItems().clear(); for (ZoomRanges zoomRange : ZoomRanges.values()) { zoomMenuButton.getItems().add(ActionUtils.createMenuItem( new Action(zoomRange.getDisplayName(), event -> controller.pushPeriod(zoomRange.getPeriod())) )); } zoomMenuButton.setText(Bundle.ViewFrame_zoomMenuButton_text()); ActionUtils.configureButton(new ZoomOut(controller), zoomOutButton); ActionUtils.configureButton(new ZoomIn(controller), zoomInButton); //register for EventBus events (tags) filteredEvents.registerForEvents(this); //listen for changes in the time range / zoom params TimeLineController.getTimeZone().addListener(timeZoneProp -> refreshTimeUI()); filteredEvents.timeRangeProperty().addListener(timeRangeProp -> refreshTimeUI()); filteredEvents.zoomParametersProperty().addListener(zoomListener); refreshTimeUI(); //populate the view refreshHistorgram(); } /** * Handle TagsUpdatedEvents by marking that the view needs to be refreshed. * * NOTE: This ViewFrame must be registered with the filteredEventsModel's * EventBus in order for this handler to be invoked. * * @param event The TagsUpdatedEvent to handle. */ @Subscribe public void handleTimeLineTagUpdate(TagsUpdatedEvent event) { hostedView.setOutOfDate(); Platform.runLater(() -> { if (notificationPane.isShowing() == false) { notificationPane.getActions().setAll(new Refresh()); notificationPane.show(Bundle.ViewFrame_tagsAddedOrDeleted(), new ImageView(INFORMATION)); } }); } /** * Handle a RefreshRequestedEvent from the events model by clearing the * refresh notification. * * NOTE: This ViewFrame must be registered with the filteredEventsModel's * EventBus in order for this handler to be invoked. * * @param event The RefreshRequestedEvent to handle. */ @Subscribe public void handleRefreshRequested(RefreshRequestedEvent event) { Platform.runLater(() -> { if (Bundle.ViewFrame_tagsAddedOrDeleted().equals(notificationPane.getText())) { notificationPane.hide(); } }); } /** * Handle a DBUpdatedEvent from the events model by refreshing the view. * * NOTE: This ViewFrame must be registered with the filteredEventsModel's * EventBus in order for this handler to be invoked. * * @param event The DBUpdatedEvent to handle. */ @Subscribe public void handleDBUpdated(DBUpdatedEvent event) { hostedView.refresh(); refreshHistorgram(); Platform.runLater(notificationPane::hide); } /** * Handle a DataSourceAddedEvent from the events model by showing a * notification. * * NOTE: This ViewFrame must be registered with the filteredEventsModel's * EventBus in order for this handler to be invoked. * * @param event The DataSourceAddedEvent to handle. */ @Subscribe @NbBundle.Messages({ "# {0} - datasource name", "ViewFrame.notification.newDataSource={0} has been added as a new datasource. The Timeline DB may be out of date."}) public void handlDataSourceAdded(DataSourceAddedEvent event) { Platform.runLater(() -> { notificationPane.getActions().setAll(new UpdateDB(controller)); notificationPane.show(Bundle.ViewFrame_notification_newDataSource(event.getDataSource().getName()), new ImageView(WARNING)); }); } /** * Handle a DataSourceAnalysisCompletedEvent from the events modelby showing * a notification. * * NOTE: This ViewFrame must be registered with the filteredEventsModel's * EventBus in order for this handler to be invoked. * * @param event The DataSourceAnalysisCompletedEvent to handle. */ @Subscribe @NbBundle.Messages({ "# {0} - datasource name", "ViewFrame.notification.analysisComplete=Analysis has finished for {0}. The Timeline DB may be out of date."}) public void handleAnalysisCompleted(DataSourceAnalysisCompletedEvent event) { Platform.runLater(() -> { notificationPane.getActions().setAll(new UpdateDB(controller)); notificationPane.show(Bundle.ViewFrame_notification_analysisComplete(event.getDataSource().getName()), new ImageView(WARNING)); }); } /** * Refresh the Histogram to represent the current state of the DB. */ @NbBundle.Messages({"ViewFrame.histogramTask.title=Rebuilding Histogram", "ViewFrame.histogramTask.preparing=Preparing", "ViewFrame.histogramTask.resetUI=Resetting UI", "ViewFrame.histogramTask.queryDb=Querying FB", "ViewFrame.histogramTask.updateUI2=Updating UI"}) synchronized private void refreshHistorgram() { if (histogramTask != null) { histogramTask.cancel(true); } histogramTask = new LoggedTask<Void>(Bundle.ViewFrame_histogramTask_title(), true) { private final Lighting lighting = new Lighting(); @Override protected Void call() throws Exception { updateMessage(ViewFrame_histogramTask_preparing()); long max = 0; final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()); final long lowerBound = rangeInfo.getLowerBound(); final long upperBound = rangeInfo.getUpperBound(); Interval timeRange = new Interval(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()), new DateTime(upperBound, TimeLineController.getJodaTimeZone())); //extend range to block bounderies (ie day, month, year) int p = 0; // progress counter //clear old data, and reset ranges and series Platform.runLater(() -> { updateMessage(ViewFrame_histogramTask_resetUI()); }); ArrayList<Long> bins = new ArrayList<>(); DateTime start = timeRange.getStart(); while (timeRange.contains(start)) { if (isCancelled()) { return null; } DateTime end = start.plus(rangeInfo.getPeriodSize().getPeriod()); final Interval interval = new Interval(start, end); //increment for next iteration start = end; updateMessage(ViewFrame_histogramTask_queryDb()); //query for current range long count = filteredEvents.getEventCounts(interval).values().stream().mapToLong(Long::valueOf).sum(); bins.add(count); max = Math.max(count, max); final double fMax = Math.log(max); final ArrayList<Long> fbins = new ArrayList<>(bins); Platform.runLater(() -> { updateMessage(ViewFrame_histogramTask_updateUI2()); histogramBox.getChildren().clear(); for (Long bin : fbins) { if (isCancelled()) { break; } Region bar = new Region(); //scale them to fit in histogram height bar.prefHeightProperty().bind(histogramBox.heightProperty().multiply(Math.log(bin)).divide(fMax)); bar.setMaxHeight(USE_PREF_SIZE); bar.setMinHeight(USE_PREF_SIZE); bar.setBackground(GRAY_BACKGROUND); bar.setOnMouseEntered((MouseEvent event) -> { Tooltip.install(bar, new Tooltip(bin.toString())); }); bar.setEffect(lighting); //they each get equal width to fill the histogram horizontally HBox.setHgrow(bar, Priority.ALWAYS); histogramBox.getChildren().add(bar); } }); } return null; } }; new Thread(histogramTask).start(); controller.monitorTask(histogramTask); } /** * Refresh the time selection UI to match the current zoom parameters. */ private void refreshTimeUI() { RangeDivisionInfo rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()); final long minTime = rangeDivisionInfo.getLowerBound(); final long maxTime = rangeDivisionInfo.getUpperBound(); long startMillis = filteredEvents.getTimeRange().getStartMillis(); long endMillis = filteredEvents.getTimeRange().getEndMillis(); if (minTime > 0 && maxTime > minTime) { Platform.runLater(() -> { startPicker.localDateTimeProperty().removeListener(startListener); endPicker.localDateTimeProperty().removeListener(endListener); rangeSlider.highValueChangingProperty().removeListener(rangeSliderListener); rangeSlider.lowValueChangingProperty().removeListener(rangeSliderListener); rangeSlider.setMax((maxTime - minTime)); rangeSlider.setLowValue(startMillis - minTime); rangeSlider.setHighValue(endMillis - minTime); startPicker.setLocalDateTime(epochMillisToLocalDateTime(startMillis)); endPicker.setLocalDateTime(epochMillisToLocalDateTime(endMillis)); rangeSlider.highValueChangingProperty().addListener(rangeSliderListener); rangeSlider.lowValueChangingProperty().addListener(rangeSliderListener); startPicker.localDateTimeProperty().addListener(startListener); endPicker.localDateTimeProperty().addListener(endListener); }); } } /** * Sync up the view shown in the UI to the one currently active according to * the controller. Swaps out the hosted AbstractTimelineView for a new one * of the correct type. */ private void syncViewMode() { ViewMode newViewMode = controller.getViewMode(); //clear out old view. if (hostedView != null) { hostedView.dispose(); } //Set a new AbstractTimeLineView as the one hosted by this ViewFrame. switch (newViewMode) { case LIST: hostedView = new ListViewPane(controller); //TODO: should remove listeners from events tree break; case COUNTS: hostedView = new CountsViewPane(controller); //TODO: should remove listeners from events tree break; case DETAIL: DetailViewPane detailViewPane = new DetailViewPane(controller); //link events tree to detailview instance. detailViewPane.setHighLightedEvents(eventsTree.getSelectedEvents()); eventsTree.setDetailViewPane(detailViewPane); hostedView = detailViewPane; break; default: throw new IllegalArgumentException("Unknown ViewMode: " + newViewMode.toString());//NON-NLS } controller.registerForEvents(hostedView); viewModeToggleGroup.setValue(newViewMode); //this selects the right toggle automatically //configure settings and time navigation nodes setViewSettingsControls(hostedView.getSettingsControls()); setTimeNavigationControls(hostedView.hasCustomTimeNavigationControls() ? hostedView.getTimeNavigationControls() : defaultTimeNavigationNodes); //do further setup of new view. ActionUtils.configureButton(new Refresh(), refreshButton);//configure new refresh action for new view hostedView.refresh(); notificationPane.setContent(hostedView); //listen to has events property and show "dialog" if it is false. hostedView.hasVisibleEventsProperty().addListener(hasEvents -> { notificationPane.setContent(hostedView.hasVisibleEvents() ? hostedView : new StackPane(hostedView, NO_EVENTS_BACKGROUND, new NoEventsDialog(() -> notificationPane.setContent(hostedView)) ) ); }); } /** * Show the given List of Nodes in the top ToolBar. Replaces any settings * Nodes that may have previously been set with the given List of Nodes. * * @param newSettingsNodes The Nodes to show in the ToolBar. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void setViewSettingsControls(List<Node> newSettingsNodes) { toolBar.getItems().removeAll(this.settingsNodes); //remove old nodes this.settingsNodes.setAll(newSettingsNodes); toolBar.getItems().addAll(SETTINGS_TOOLBAR_INSERTION_INDEX, settingsNodes); } /** * Show the given List of Nodes in the time range ToolBar. Replaces any * Nodes that may have previously been set with the given List of Nodes. * * @param timeNavigationNodes The Nodes to show in the time range ToolBar. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void setTimeNavigationControls(List<Node> timeNavigationNodes) { timeRangeToolBar.getItems().removeAll(this.timeNavigationNodes); //remove old nodes this.timeNavigationNodes.setAll(timeNavigationNodes); timeRangeToolBar.getItems().addAll(TIME_TOOLBAR_INSERTION_INDEX, timeNavigationNodes); } @NbBundle.Messages("NoEventsDialog.titledPane.text=No Visible Events") private class NoEventsDialog extends StackPane { @FXML private TitledPane titledPane; @FXML private Button backButton; @FXML private Button resetFiltersButton; @FXML private Button dismissButton; @FXML private Button zoomButton; @FXML private Label noEventsDialogLabel; private final Runnable closeCallback; private NoEventsDialog(Runnable closeCallback) { this.closeCallback = closeCallback; FXMLConstructor.construct(this, "NoEventsDialog.fxml"); //NON-NLS } @FXML @NbBundle.Messages("ViewFrame.noEventsDialogLabel.text=There are no events visible with the current zoom / filter settings.") void initialize() { assert resetFiltersButton != null : "fx:id=\"resetFiltersButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'."; //NON-NLS assert dismissButton != null : "fx:id=\"dismissButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'."; //NON-NLS assert zoomButton != null : "fx:id=\"zoomButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'."; //NON-NLS titledPane.setText(Bundle.NoEventsDialog_titledPane_text()); noEventsDialogLabel.setText(Bundle.ViewFrame_noEventsDialogLabel_text()); dismissButton.setOnAction(actionEvent -> closeCallback.run()); ActionUtils.configureButton(new ZoomToEvents(controller), zoomButton); ActionUtils.configureButton(new Back(controller), backButton); ActionUtils.configureButton(new ResetFilters(controller), resetFiltersButton); } } /** * Base class for listeners that listen to a LocalDateTimeTextField and push * the selected LocalDateTime as start/end to the timelinecontroller. */ private class PickerListener implements InvalidationListener { private final BiFunction< Interval, Long, Interval> intervalMapper; private final Supplier<LocalDateTimeTextField> pickerSupplier; PickerListener(Supplier<LocalDateTimeTextField> pickerSupplier, BiFunction<Interval, Long, Interval> intervalMapper) { this.pickerSupplier = pickerSupplier; this.intervalMapper = intervalMapper; } @Override public void invalidated(Observable observable) { LocalDateTime pickerTime = pickerSupplier.get().getLocalDateTime(); if (pickerTime != null) { controller.pushTimeRange(intervalMapper.apply(filteredEvents.timeRangeProperty().get(), localDateTimeToEpochMilli(pickerTime))); Platform.runLater(ViewFrame.this::refreshTimeUI); } } } /** * callback that disabled date/times outside the span of the current case. */ private class LocalDateDisabler implements Callback<LocalDateTimePicker.LocalDateTimeRange, Void> { @Override public Void call(LocalDateTimePicker.LocalDateTimeRange viewedRange) { startPicker.disabledLocalDateTimes().clear(); endPicker.disabledLocalDateTimes().clear(); //all events in the case are contained in this interval Interval spanningInterval = filteredEvents.getSpanningInterval(); long spanStartMillis = spanningInterval.getStartMillis(); long spaneEndMillis = spanningInterval.getEndMillis(); LocalDate rangeStartLocalDate = viewedRange.getStartLocalDateTime().toLocalDate(); LocalDate rangeEndLocalDate = viewedRange.getEndLocalDateTime().toLocalDate().plusDays(1); //iterate over days of the displayed range and disable ones not in spanning interval for (LocalDate dt = rangeStartLocalDate; false == dt.isAfter(rangeEndLocalDate); dt = dt.plusDays(1)) { long startOfDay = dt.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); long endOfDay = dt.plusDays(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); //if no part of day is within spanning interval, add that date the list of disabled dates. if (endOfDay < spanStartMillis || startOfDay > spaneEndMillis) { startPicker.disabledLocalDateTimes().add(dt.atStartOfDay()); endPicker.disabledLocalDateTimes().add(dt.atStartOfDay()); } } return null; } } /** * Callback that validates that selected date/times are in the spanning * interval for this case, and resets the textbox if invalid date/time was * entered. */ private class LocalDateTimeValidator implements Callback<LocalDateTime, Boolean> { /** * picker to reset if invalid info was entered */ private final LocalDateTimeTextField picker; LocalDateTimeValidator(LocalDateTimeTextField picker) { this.picker = picker; } @Override public Boolean call(LocalDateTime param) { long epochMilli = localDateTimeToEpochMilli(param); if (filteredEvents.getSpanningInterval().contains(epochMilli)) { return true; } else { if (picker.isPickerShowing() == false) { //if the user typed an in valid date, reset the text box to the selected date. picker.setDisplayedLocalDateTime(picker.getLocalDateTime()); } return false; } } } /** * Action that refreshes the View. */ private class Refresh extends Action { @NbBundle.Messages({ "ViewFrame.refresh.text=Refresh View", "ViewFrame.refresh.longText=Refresh the view to include information that is in the DB but not displayed, such as newly updated tags."}) Refresh() { super(Bundle.ViewFrame_refresh_text()); setLongText(Bundle.ViewFrame_refresh_longText()); setGraphic(new ImageView(REFRESH)); setEventHandler(actionEvent -> filteredEvents.postRefreshRequest()); disabledProperty().bind(hostedView.outOfDateProperty().not()); } } }