diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index 5d889f68d2..58e22aef07 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -567,7 +567,7 @@ public class TimeLineController { } synchronized public void pushTimeRange(Interval timeRange) { -// timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange); + timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange); ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get(); if (currentZoom == null) { advance(InitialZoomState.withTimeRange(timeRange)); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java index 8faa1bfe53..e3a06617c7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java @@ -32,6 +32,9 @@ import java.util.function.Consumer; import java.util.logging.Level; import javafx.embed.swing.SwingFXUtils; import javafx.event.ActionEvent; +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.image.WritableImage; import javafx.stage.DirectoryChooser; import javafx.util.Pair; @@ -48,15 +51,19 @@ import org.sleuthkit.datamodel.TskCoreException; */ public class SaveSnapshotAsReport extends Action { + private static final Image SNAP_SHOT = new Image("org/sleuthkit/autopsy/timeline/images/image.png", 16, 16, true, true); private static final String HTML_EXT = ".html"; private static final String REPORT_IMAGE_EXTENSION = ".png"; private static final Logger LOGGER = Logger.getLogger(SaveSnapshotAsReport.class.getName()); - @NbBundle.Messages({"SaveSnapshot.action.name.text=save snapshot", - "SaveSnapshot.fileChoose.title.text=Save snapshot to"}) - public SaveSnapshotAsReport(TimeLineController controller, WritableImage snapshot) { + @NbBundle.Messages({"SaveSnapshot.action.name.text=Snapshot", + "SaveSnapshot.action.longText=Save a screen capture of the visualization as a report.", + "SaveSnapshot.fileChoose.title.text=Save snapshot to",}) + public SaveSnapshotAsReport(TimeLineController controller, Node node) { super(Bundle.SaveSnapshot_action_name_text()); + setLongText(Bundle.SaveSnapshot_action_longText()); + setGraphic(new ImageView(SNAP_SHOT)); setEventHandler(new Consumer() { @Override @@ -86,6 +93,7 @@ public class SaveSnapshotAsReport extends Action { //save snapshot as png try { + WritableImage snapshot = node.snapshot(null, null); ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", new File(reportPath, reportName + REPORT_IMAGE_EXTENSION)); // NON-NLS } catch (IOException ex) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomIn.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomIn.java index d1f76c3c7e..bd58c3ccb7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomIn.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomIn.java @@ -31,7 +31,7 @@ public class ZoomIn extends Action { private static final Image MAGNIFIER_IN = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in-green.png"); //NOI18N - @NbBundle.Messages({"ZoomIn.longText=Zoom in to view half as much time.", + @NbBundle.Messages({"ZoomIn.longText=Zoom in to view about half as much time.", "ZoomIn.action.text=Zoom in"}) public ZoomIn(TimeLineController controller) { super(Bundle.ZoomIn_action_text()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java index 8818ff4557..2924fcc6ec 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java @@ -33,7 +33,7 @@ public class ZoomOut extends Action { private static final Image MAGNIFIER_OUT = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png"); //NOI18N - @NbBundle.Messages({"ZoomOut.longText=Zoom out to view 50% more time.", + @NbBundle.Messages({"ZoomOut.longText=Zoom out to view about 50% more time.", "ZoomOut.action.text=Zoom out"}) public ZoomOut(TimeLineController controller) { super(Bundle.ZoomOut_action_text()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties index 521cf76e4f..2d70d05a42 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties @@ -41,18 +41,11 @@ VisualizationPanel.histogramTask.preparing=preparing VisualizationPanel.histogramTask.resetUI=resetting ui VisualizationPanel.histogramTask.queryDb=querying db VisualizationPanel.histogramTask.updateUI2=updating ui -VisualizationPanel.visualizationModeLabel.text=Visualization Mode\: StatusBar.refreshLabel.text=New events may be available, re-open the timeline to refresh. StatusBar.messageLabel.text=Label StatusBar.taskLabel.text=Label VisualizationPanel.noEventsDialogLabel.text=There are no events visible with the current zoom / filter settings. VisualizationPanel.zoomButton.text=Zoom to events -VisualizationPanel.startLabel.text=Start\: -VisualizationPanel.endLabel.text=End\: TimeZonePanel.localRadio.text=Local Time Zone TimeZonePanel.otherRadio.text=GMT / UTC -VisualizationPanel.zoomMenuButton.text=Zoom in/out to -VisualizationPanel.snapShotButton.text=Screenshot -VisualizationPanel.detailsToggle.text=Details -VisualizationPanel.countsToggle.text=Counts VisualizationPanel.resetFiltersButton.text=Reset all filters \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java index 2be135a9f5..b7a35e8738 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java @@ -19,12 +19,13 @@ package org.sleuthkit.autopsy.timeline.ui; import com.google.common.eventbus.Subscribe; -import java.net.URL; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.ZonedDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; -import java.util.ResourceBundle; +import java.util.function.BiFunction; +import java.util.function.Supplier; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; @@ -36,8 +37,6 @@ import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.MenuButton; -import javafx.scene.control.MenuItem; -import javafx.scene.control.Separator; import javafx.scene.control.TitledPane; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleButton; @@ -58,7 +57,10 @@ 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 org.controlsfx.control.NotificationPane; import org.controlsfx.control.RangeSlider; @@ -81,8 +83,6 @@ import org.sleuthkit.autopsy.timeline.actions.ZoomToEvents; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.events.TagsUpdatedEvent; import org.sleuthkit.autopsy.timeline.filters.TagsFilter; -import static org.sleuthkit.autopsy.timeline.ui.Bundle.VisualizationPanel_refresh; -import static org.sleuthkit.autopsy.timeline.ui.Bundle.VisualizationPanel_tagsAddedOrDeleted; 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; @@ -91,29 +91,34 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; /** * A container for an {@link AbstractVisualizationPane}, has a toolbar on top to * hold settings widgets supplied by contained {@link AbstAbstractVisualization} - * and the histogram / timeselection on bottom. Also supplies containers for + * and, the histogram / time selection on bottom. Also supplies containers for * replacement axis to contained {@link AbstractAbstractVisualization} * * TODO: refactor common code out of histogram and CountsView? -jm */ final public class VisualizationPanel extends BorderPane { + private static final Logger LOGGER = Logger.getLogger(VisualizationPanel.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 REFRESH = new Image("org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png"); // NON-NLS - - private static final Logger LOGGER = Logger.getLogger(VisualizationPanel.class.getName()); + private static final Background background = new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)); @GuardedBy("this") private LoggedTask histogramTask; private final EventsTree eventsTree; - private AbstractVisualizationPane visualization; - //// range slider and histogram componenets + /** + * hbox that contains the histogram bars. //TODO: abstract this into a + * seperate class, and/or use a real bar chart? + */ @FXML private HBox histogramBox; - + /** + * stack pane that superimposes rangeslider over histogram + */ @FXML private StackPane rangeHistogramStack; @@ -123,97 +128,128 @@ final public class VisualizationPanel extends BorderPane { @FXML private MenuButton zoomMenuButton; - @FXML - private Separator rightSeperator; - - @FXML - private Separator leftSeperator; - @FXML private Button zoomOutButton; - @FXML private Button zoomInButton; - @FXML private LocalDateTimeTextField startPicker; - @FXML private LocalDateTimeTextField endPicker; + @FXML + private Label startLabel; + @FXML + private Label endLabel; //// replacemetn axis label componenets @FXML private Pane partPane; - @FXML private Pane contextPane; - @FXML private Region spacer; //// header toolbar componenets @FXML private ToolBar toolBar; - @FXML private ToggleButton countsToggle; - @FXML private ToggleButton detailsToggle; - @FXML private Button snapShotButton; @FXML private Label visualizationModeLabel; - @FXML - private Label startLabel; - - @FXML - private Label endLabel; + /** + * wraps contained visualization so that we can show notifications over it. + */ private final NotificationPane notificationPane = new NotificationPane(); - private TimeLineController controller; + private final TimeLineController controller; + private final FilteredEventsModel filteredEvents; - private FilteredEventsModel filteredEvents; - - private final ChangeListener rangeSliderListener = - (observable1, oldValue, newValue) -> { - if (rangeSlider.isHighValueChanging() == false && rangeSlider.isLowValueChanging() == false) { - Long minTime = filteredEvents.getMinTime() * 1000; - controller.pushTimeRange(new Interval( - new Double(rangeSlider.getLowValue() + minTime).longValue(), - new Double(rangeSlider.getHighValue() + minTime).longValue(), - DateTimeZone.UTC)); - } - }; - - private final InvalidationListener endListener = (Observable observable) -> { - if (endPicker.getLocalDateTime() != null) { - controller.pushTimeRange(VisualizationPanel.this.filteredEvents.timeRangeProperty().get().withEndMillis( - ZonedDateTime.of(endPicker.getLocalDateTime(), TimeLineController.getTimeZoneID()).toInstant().toEpochMilli())); + /** + * listen to change in range slider selected time and push to controller. + * waits until the user releases thumb to send controller. + */ + private final InvalidationListener rangeSliderListener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + if (rangeSlider.isHighValueChanging() == false + && rangeSlider.isLowValueChanging() == false) { + Long minTime = filteredEvents.getMinTime() * 1000; + controller.pushTimeRange(new Interval( + new Double(rangeSlider.getLowValue() + minTime).longValue(), + new Double(rangeSlider.getHighValue() + minTime).longValue(), + DateTimeZone.UTC)); + } } }; - private final InvalidationListener startListener = (Observable observable) -> { - if (startPicker.getLocalDateTime() != null) { - controller.pushTimeRange(VisualizationPanel.this.filteredEvents.timeRangeProperty().get().withStartMillis( - ZonedDateTime.of(startPicker.getLocalDateTime(), TimeLineController.getTimeZoneID()).toInstant().toEpochMilli())); + /** + * listens to timerange of filtered events model and updates UI to match + */ + final private InvalidationListener timeRangeInvalidationListener = new InvalidationListener() { + public void invalidated(Observable observable) { + refreshTimeUI(filteredEvents.timeRangeProperty().get()); } }; - static private final Background background = new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)); + /** + * hides the notification pane on any event + */ + private final InvalidationListener zoomListener = any -> notificationPane.hide(); - static private final Lighting lighting = new Lighting(); + /** + * listen to change in end time picker and push to controller + */ + private final InvalidationListener endListener = new PickerListener(() -> endPicker, Interval::withEndMillis); - public VisualizationPanel(TimeLineController controller, EventsTree eventsTree) { + /** + * 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 CURERNT + * TIMEZONE FROM TIMELINECONTROLLER + * + * @param localDateTime + * + * @return the given localdatetime as epoch millis + */ + private static long localDateTimeToEpochMilli(LocalDateTime localDateTime) { + return localDateTime.atZone(TimeLineController.getTimeZoneID()).toInstant().toEpochMilli(); + } + + /** + * convert the given epoch millis to a LocalDateTime USING THE CURERNT + * TIMEZONE FROM TIMELINECONTROLLER + * + * @param millis + * + * @return the given epoch millis as a LocalDateTime + */ + private static LocalDateTime epochMillisToLocalDateTime(long millis) { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), TimeLineController.getTimeZoneID()); + } + + public VisualizationPanel(@Nonnull TimeLineController controller, @Nonnull EventsTree eventsTree) { this.controller = controller; + this.filteredEvents = controller.getEventsModel(); this.eventsTree = eventsTree; FXMLConstructor.construct(this, "VisualizationPanel.fxml"); // NON-NLS } @FXML // This method is called by the FXMLLoader when initialization is complete - @NbBundle.Messages("VisualizationPanel.refresh=refresh") + @NbBundle.Messages({"VisualizationPanel.refresh=refresh", + "VisualizationPanel.visualizationModeLabel.text=Visualization Mode:", + "VisualizationPanel.startLabel.text=Start:", + "VisualizationPanel.endLabel.text=End:", + "VisualizationPanel.countsToggle.text=Counts", + "VisualizationPanel.detailsToggle.text=Details", + "VisualizationPanel.zoomMenuButton.text=Zoom in/out to"}) 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 @@ -222,8 +258,9 @@ final public class VisualizationPanel extends BorderPane { 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 + //configure notification pane notificationPane.getStyleClass().add(NotificationPane.STYLE_CLASS_DARK); - notificationPane.getActions().setAll(new Action(VisualizationPanel_refresh()) { + notificationPane.getActions().setAll(new Action(Bundle.VisualizationPanel_refresh()) { { setGraphic(new ImageView(REFRESH)); setEventHandler((ActionEvent t) -> { @@ -233,23 +270,20 @@ final public class VisualizationPanel extends BorderPane { } }); setCenter(notificationPane); - visualizationModeLabel.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.visualizationModeLabel.text")); // NON-NLS - startLabel.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.startLabel.text")); // NON-NLS - endLabel.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.endLabel.text")); // NON-NLS - HBox.setHgrow(leftSeperator, Priority.ALWAYS); - HBox.setHgrow(rightSeperator, Priority.ALWAYS); - ChangeListener toggleListener = (ObservableValue observable, - Toggle oldValue, - Toggle newValue) -> { - if (newValue == null) { - countsToggle.getToggleGroup().selectToggle(oldValue != null ? oldValue : countsToggle); - } else if (newValue == countsToggle && oldValue != null) { - controller.setViewMode(VisualizationMode.COUNTS); - } else if (newValue == detailsToggle && oldValue != null) { - controller.setViewMode(VisualizationMode.DETAIL); - } - }; + //configure visualization mode toggle + visualizationModeLabel.setText(Bundle.VisualizationPanel_visualizationModeLabel_text()); + countsToggle.setText(Bundle.VisualizationPanel_countsToggle_text()); + detailsToggle.setText(Bundle.VisualizationPanel_detailsToggle_text()); + ChangeListener toggleListener = (ObservableValue observable, Toggle oldValue, Toggle newValue) -> { + if (newValue == null) { + countsToggle.getToggleGroup().selectToggle(oldValue != null ? oldValue : countsToggle); + } else if (newValue == countsToggle && oldValue != null) { + controller.setViewMode(VisualizationMode.COUNTS); + } else if (newValue == detailsToggle && oldValue != null) { + controller.setViewMode(VisualizationMode.DETAIL); + } + }; if (countsToggle.getToggleGroup() != null) { countsToggle.getToggleGroup().selectedToggleProperty().addListener(toggleListener); @@ -258,14 +292,34 @@ final public class VisualizationPanel extends BorderPane { countsToggle.getToggleGroup().selectedToggleProperty().addListener(toggleListener); }); } - countsToggle.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.countsToggle.text")); // NON-NLS - detailsToggle.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.detailsToggle.text")); // NON-NLS + controller.viewModeProperty().addListener(observable -> setViewMode(controller.viewModeProperty().get())); + setViewMode(controller.viewModeProperty().get()); + + //configure snapshor button / action + ActionUtils.configureButton(new SaveSnapshotAsReport(controller, VisualizationPanel.this), snapShotButton); + + /////configure start and end pickers + startLabel.setText(Bundle.VisualizationPanel_startLabel_text()); + endLabel.setText(Bundle.VisualizationPanel_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); /* @@ -275,56 +329,39 @@ final public class VisualizationPanel extends BorderPane { */ histogramBox.setStyle(" -fx-padding: 0,0.5em,0,.5em; "); // NON-NLS + //configure zoom buttons zoomMenuButton.getItems().clear(); - for (ZoomRanges b : ZoomRanges.values()) { - MenuItem menuItem = new MenuItem(b.getDisplayName()); - menuItem.setOnAction((event) -> { - if (b != ZoomRanges.ALL) { - controller.pushPeriod(b.getPeriod()); - } else { - controller.showFullRange(); - } - }); - zoomMenuButton.getItems().add(menuItem); + for (ZoomRanges zoomRange : ZoomRanges.values()) { + zoomMenuButton.getItems().add(ActionUtils.createMenuItem( + new Action(zoomRange.getDisplayName(), event -> { + if (zoomRange != ZoomRanges.ALL) { + controller.pushPeriod(zoomRange.getPeriod()); + } else { + controller.showFullRange(); + } + }))); } - zoomMenuButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomMenuButton.text")); // NON-NLS - - snapShotButton.setOnAction(event -> - this.snapshot(snapShotResult -> { - new SaveSnapshotAsReport(controller, snapShotResult.getImage()).handle(event); - return null; - }, null, null) - ); - - snapShotButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.snapShotButton.text")); // NON-NLS - if (this.filteredEvents != null && this.filteredEvents != controller.getEventsModel()) { - this.filteredEvents.unRegisterForEvents(this); - this.filteredEvents.timeRangeProperty().removeListener(timeRangeInvalidationListener); - this.filteredEvents.zoomParametersProperty().removeListener(zoomListener); - } - if (this.filteredEvents != controller.getEventsModel()) { - controller.getEventsModel().registerForEvents(this); - controller.getEventsModel().timeRangeProperty().addListener(timeRangeInvalidationListener); - controller.getEventsModel().zoomParametersProperty().addListener(zoomListener); - } - - this.filteredEvents = controller.getEventsModel(); - refreshTimeUI(controller.getEventsModel().timeRangeProperty().get()); + zoomMenuButton.setText(Bundle.VisualizationPanel_zoomMenuButton_text()); ActionUtils.configureButton(new ZoomOut(controller), zoomOutButton); ActionUtils.configureButton(new ZoomIn(controller), zoomInButton); - setViewMode(controller.viewModeProperty().get()); - controller.getNeedsHistogramRebuild().addListener((ObservableValue observable, Boolean oldValue, Boolean newValue) -> { + //register for EventBus events (tags) + filteredEvents.registerForEvents(this); + + //listen for changes in the time range / zoom params + TimeLineController.getTimeZone().addListener(timeRangeInvalidationListener); + filteredEvents.timeRangeProperty().addListener(timeRangeInvalidationListener); + filteredEvents.zoomParametersProperty().addListener(zoomListener); + refreshTimeUI(filteredEvents.timeRangeProperty().get()); //populate the viz + + //this should use an event(EventBus) , not this weird observable pattern + controller.getNeedsHistogramRebuild().addListener((observable, oldValue, newValue) -> { if (newValue) { refreshHistorgram(); } }); - - controller.viewModeProperty().addListener((ObservableValue ov, VisualizationMode t, VisualizationMode t1) -> { - setViewMode(t1); - }); - TimeLineController.getTimeZone().addListener(timeRangeInvalidationListener); refreshHistorgram(); + } private void setViewMode(VisualizationMode visualizationMode) { @@ -382,19 +419,19 @@ final public class VisualizationPanel extends BorderPane { TagsFilter tagsFilter = filteredEvents.getFilter().getTagsFilter(); if (tagsFilter.isSelected() && tagsFilter.isDisabled() == false) { Platform.runLater(() -> { - notificationPane.show(VisualizationPanel_tagsAddedOrDeleted(), new ImageView(INFORMATION)); + notificationPane.show(Bundle.VisualizationPanel_tagsAddedOrDeleted(), new ImageView(INFORMATION)); }); } } synchronized private void refreshHistorgram() { - if (histogramTask != null) { histogramTask.cancel(true); } histogramTask = new LoggedTask( NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.title"), true) { // NON-NLS + private final Lighting lighting = new Lighting(); @Override protected Void call() throws Exception { @@ -471,18 +508,10 @@ final public class VisualizationPanel extends BorderPane { controller.monitorTask(histogramTask); } - private InvalidationListener timeRangeInvalidationListener = (Observable observable) -> { - refreshTimeUI(filteredEvents.timeRangeProperty().get()); - }; - - private InvalidationListener zoomListener = (Observable observable) -> { - notificationPane.hide(); - }; - private void refreshTimeUI(Interval interval) { RangeDivisionInfo rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()); - final Long minTime = rangeDivisionInfo.getLowerBound(); + final long minTime = rangeDivisionInfo.getLowerBound(); final long maxTime = rangeDivisionInfo.getUpperBound(); if (minTime > 0 && maxTime > minTime) { @@ -493,11 +522,13 @@ final public class VisualizationPanel extends BorderPane { rangeSlider.highValueChangingProperty().removeListener(rangeSliderListener); rangeSlider.lowValueChangingProperty().removeListener(rangeSliderListener); - rangeSlider.setMax((Long) (maxTime - minTime)); - rangeSlider.setHighValue(interval.getEndMillis() - minTime); - rangeSlider.setLowValue(interval.getStartMillis() - minTime); - endPicker.setLocalDateTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(interval.getEndMillis()), TimeLineController.getTimeZoneID())); - startPicker.setLocalDateTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(interval.getStartMillis()), TimeLineController.getTimeZoneID())); + rangeSlider.setMax((maxTime - minTime)); + long startMillis = interval.getStartMillis(); + long endMillis = interval.getEndMillis(); + rangeSlider.setLowValue(startMillis - minTime); + rangeSlider.setHighValue(endMillis - minTime); + startPicker.setLocalDateTime(epochMillisToLocalDateTime(startMillis)); + endPicker.setLocalDateTime(epochMillisToLocalDateTime(endMillis)); rangeSlider.highValueChangingProperty().addListener(rangeSliderListener); rangeSlider.lowValueChangingProperty().addListener(rangeSliderListener); @@ -509,26 +540,17 @@ final public class VisualizationPanel extends BorderPane { private class NoEventsDialog extends TitledPane { - private final Runnable closeCallback; - - @FXML // ResourceBundle that was given to the FXMLLoader - private ResourceBundle resources; - - @FXML // URL location of the FXML file that was given to the FXMLLoader - private URL location; - @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 @@ -550,4 +572,90 @@ final public class VisualizationPanel extends BorderPane { resetFiltersButton.setText(NbBundle.getMessage(NoEventsDialog.class, "VisualizationPanel.resetFiltersButton.text")); // NON-NLS } } + + /** + * 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 pickerSupplier; + + PickerListener(Supplier pickerSupplier, BiFunction 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(() -> refreshTimeUI(filteredEvents.timeRangeProperty().get())); + } + } + } + + /** + * callback that disabled date/times outside the span of the current case. + */ + private class LocalDateDisabler implements Callback { + + @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 { + + /** + * 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; + } + } + } }