Merge pull request #1682 from millmanorama/TL-text-zoom-stack-overflow

further limit time zoom to events in case. limit time range selection…
This commit is contained in:
Richard Cordovano 2015-10-30 10:00:26 -04:00
commit 55a6b18d4c
6 changed files with 267 additions and 158 deletions

View File

@ -567,7 +567,7 @@ public class TimeLineController {
} }
synchronized public void pushTimeRange(Interval timeRange) { synchronized public void pushTimeRange(Interval timeRange) {
// timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange); timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange);
ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get(); ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
if (currentZoom == null) { if (currentZoom == null) {
advance(InitialZoomState.withTimeRange(timeRange)); advance(InitialZoomState.withTimeRange(timeRange));

View File

@ -32,6 +32,9 @@ import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.embed.swing.SwingFXUtils; import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent; 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.scene.image.WritableImage;
import javafx.stage.DirectoryChooser; import javafx.stage.DirectoryChooser;
import javafx.util.Pair; import javafx.util.Pair;
@ -48,15 +51,19 @@ import org.sleuthkit.datamodel.TskCoreException;
*/ */
public class SaveSnapshotAsReport extends Action { 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 HTML_EXT = ".html";
private static final String REPORT_IMAGE_EXTENSION = ".png"; private static final String REPORT_IMAGE_EXTENSION = ".png";
private static final Logger LOGGER = Logger.getLogger(SaveSnapshotAsReport.class.getName()); private static final Logger LOGGER = Logger.getLogger(SaveSnapshotAsReport.class.getName());
@NbBundle.Messages({"SaveSnapshot.action.name.text=save snapshot", @NbBundle.Messages({"SaveSnapshot.action.name.text=Snapshot",
"SaveSnapshot.fileChoose.title.text=Save snapshot to"}) "SaveSnapshot.action.longText=Save a screen capture of the visualization as a report.",
public SaveSnapshotAsReport(TimeLineController controller, WritableImage snapshot) { "SaveSnapshot.fileChoose.title.text=Save snapshot to",})
public SaveSnapshotAsReport(TimeLineController controller, Node node) {
super(Bundle.SaveSnapshot_action_name_text()); super(Bundle.SaveSnapshot_action_name_text());
setLongText(Bundle.SaveSnapshot_action_longText());
setGraphic(new ImageView(SNAP_SHOT));
setEventHandler(new Consumer<ActionEvent>() { setEventHandler(new Consumer<ActionEvent>() {
@Override @Override
@ -86,6 +93,7 @@ public class SaveSnapshotAsReport extends Action {
//save snapshot as png //save snapshot as png
try { try {
WritableImage snapshot = node.snapshot(null, null);
ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png",
new File(reportPath, reportName + REPORT_IMAGE_EXTENSION)); // NON-NLS new File(reportPath, reportName + REPORT_IMAGE_EXTENSION)); // NON-NLS
} catch (IOException ex) { } catch (IOException ex) {

View File

@ -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 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"}) "ZoomIn.action.text=Zoom in"})
public ZoomIn(TimeLineController controller) { public ZoomIn(TimeLineController controller) {
super(Bundle.ZoomIn_action_text()); super(Bundle.ZoomIn_action_text());

View File

@ -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 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"}) "ZoomOut.action.text=Zoom out"})
public ZoomOut(TimeLineController controller) { public ZoomOut(TimeLineController controller) {
super(Bundle.ZoomOut_action_text()); super(Bundle.ZoomOut_action_text());

View File

@ -41,18 +41,11 @@ VisualizationPanel.histogramTask.preparing=preparing
VisualizationPanel.histogramTask.resetUI=resetting ui VisualizationPanel.histogramTask.resetUI=resetting ui
VisualizationPanel.histogramTask.queryDb=querying db VisualizationPanel.histogramTask.queryDb=querying db
VisualizationPanel.histogramTask.updateUI2=updating ui 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.refreshLabel.text=New events may be available, re-open the timeline to refresh.
StatusBar.messageLabel.text=Label StatusBar.messageLabel.text=Label
StatusBar.taskLabel.text=Label StatusBar.taskLabel.text=Label
VisualizationPanel.noEventsDialogLabel.text=There are no events visible with the current zoom / filter settings. VisualizationPanel.noEventsDialogLabel.text=There are no events visible with the current zoom / filter settings.
VisualizationPanel.zoomButton.text=Zoom to events VisualizationPanel.zoomButton.text=Zoom to events
VisualizationPanel.startLabel.text=Start\:
VisualizationPanel.endLabel.text=End\:
TimeZonePanel.localRadio.text=Local Time Zone TimeZonePanel.localRadio.text=Local Time Zone
TimeZonePanel.otherRadio.text=GMT / UTC 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 VisualizationPanel.resetFiltersButton.text=Reset all filters

View File

@ -19,12 +19,13 @@
package org.sleuthkit.autopsy.timeline.ui; package org.sleuthkit.autopsy.timeline.ui;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import java.net.URL;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZonedDateTime; import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.ResourceBundle; import java.util.function.BiFunction;
import java.util.function.Supplier;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
import javafx.beans.Observable; import javafx.beans.Observable;
@ -36,8 +37,6 @@ import javafx.geometry.Insets;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.MenuButton; import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Separator;
import javafx.scene.control.TitledPane; import javafx.scene.control.TitledPane;
import javafx.scene.control.Toggle; import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleButton;
@ -58,7 +57,10 @@ import javafx.scene.layout.Region;
import static javafx.scene.layout.Region.USE_PREF_SIZE; import static javafx.scene.layout.Region.USE_PREF_SIZE;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.util.Callback;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.GuardedBy;
import jfxtras.scene.control.LocalDateTimePicker;
import jfxtras.scene.control.LocalDateTimeTextField; import jfxtras.scene.control.LocalDateTimeTextField;
import org.controlsfx.control.NotificationPane; import org.controlsfx.control.NotificationPane;
import org.controlsfx.control.RangeSlider; 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.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.events.TagsUpdatedEvent; import org.sleuthkit.autopsy.timeline.events.TagsUpdatedEvent;
import org.sleuthkit.autopsy.timeline.filters.TagsFilter; 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.countsview.CountsViewPane;
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
import org.sleuthkit.autopsy.timeline.ui.detailview.tree.EventsTree; 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 * A container for an {@link AbstractVisualizationPane}, has a toolbar on top to
* hold settings widgets supplied by contained {@link AbstAbstractVisualization} * 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} * replacement axis to contained {@link AbstractAbstractVisualization}
* *
* TODO: refactor common code out of histogram and CountsView? -jm * TODO: refactor common code out of histogram and CountsView? -jm
*/ */
final public class VisualizationPanel extends BorderPane { 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 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 Image REFRESH = new Image("org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png"); // NON-NLS
private static final Background background = new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY));
private static final Logger LOGGER = Logger.getLogger(VisualizationPanel.class.getName());
@GuardedBy("this") @GuardedBy("this")
private LoggedTask<Void> histogramTask; private LoggedTask<Void> histogramTask;
private final EventsTree eventsTree; private final EventsTree eventsTree;
private AbstractVisualizationPane<?, ?, ?, ?> visualization; private AbstractVisualizationPane<?, ?, ?, ?> visualization;
//// range slider and histogram componenets //// 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 @FXML
private HBox histogramBox; private HBox histogramBox;
/**
* stack pane that superimposes rangeslider over histogram
*/
@FXML @FXML
private StackPane rangeHistogramStack; private StackPane rangeHistogramStack;
@ -123,97 +128,128 @@ final public class VisualizationPanel extends BorderPane {
@FXML @FXML
private MenuButton zoomMenuButton; private MenuButton zoomMenuButton;
@FXML
private Separator rightSeperator;
@FXML
private Separator leftSeperator;
@FXML @FXML
private Button zoomOutButton; private Button zoomOutButton;
@FXML @FXML
private Button zoomInButton; private Button zoomInButton;
@FXML @FXML
private LocalDateTimeTextField startPicker; private LocalDateTimeTextField startPicker;
@FXML @FXML
private LocalDateTimeTextField endPicker; private LocalDateTimeTextField endPicker;
@FXML
private Label startLabel;
@FXML
private Label endLabel;
//// replacemetn axis label componenets //// replacemetn axis label componenets
@FXML @FXML
private Pane partPane; private Pane partPane;
@FXML @FXML
private Pane contextPane; private Pane contextPane;
@FXML @FXML
private Region spacer; private Region spacer;
//// header toolbar componenets //// header toolbar componenets
@FXML @FXML
private ToolBar toolBar; private ToolBar toolBar;
@FXML @FXML
private ToggleButton countsToggle; private ToggleButton countsToggle;
@FXML @FXML
private ToggleButton detailsToggle; private ToggleButton detailsToggle;
@FXML @FXML
private Button snapShotButton; private Button snapShotButton;
@FXML @FXML
private Label visualizationModeLabel; 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 final NotificationPane notificationPane = new NotificationPane();
private TimeLineController controller; private final TimeLineController controller;
private final FilteredEventsModel filteredEvents;
private FilteredEventsModel filteredEvents; /**
* listen to change in range slider selected time and push to controller.
private final ChangeListener<Object> rangeSliderListener = * waits until the user releases thumb to send controller.
(observable1, oldValue, newValue) -> { */
if (rangeSlider.isHighValueChanging() == false && rangeSlider.isLowValueChanging() == false) { private final InvalidationListener rangeSliderListener = new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
if (rangeSlider.isHighValueChanging() == false
&& rangeSlider.isLowValueChanging() == false) {
Long minTime = filteredEvents.getMinTime() * 1000; Long minTime = filteredEvents.getMinTime() * 1000;
controller.pushTimeRange(new Interval( controller.pushTimeRange(new Interval(
new Double(rangeSlider.getLowValue() + minTime).longValue(), new Double(rangeSlider.getLowValue() + minTime).longValue(),
new Double(rangeSlider.getHighValue() + minTime).longValue(), new Double(rangeSlider.getHighValue() + minTime).longValue(),
DateTimeZone.UTC)); 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()));
} }
}; };
private final InvalidationListener startListener = (Observable observable) -> { /**
if (startPicker.getLocalDateTime() != null) { * listens to timerange of filtered events model and updates UI to match
controller.pushTimeRange(VisualizationPanel.this.filteredEvents.timeRangeProperty().get().withStartMillis( */
ZonedDateTime.of(startPicker.getLocalDateTime(), TimeLineController.getTimeZoneID()).toInstant().toEpochMilli())); 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.controller = controller;
this.filteredEvents = controller.getEventsModel();
this.eventsTree = eventsTree; this.eventsTree = eventsTree;
FXMLConstructor.construct(this, "VisualizationPanel.fxml"); // NON-NLS FXMLConstructor.construct(this, "VisualizationPanel.fxml"); // NON-NLS
} }
@FXML // This method is called by the FXMLLoader when initialization is complete @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() { void initialize() {
assert endPicker != null : "fx:id=\"endPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'."; // NON-NLS 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 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 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 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.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)); setGraphic(new ImageView(REFRESH));
setEventHandler((ActionEvent t) -> { setEventHandler((ActionEvent t) -> {
@ -233,15 +270,12 @@ final public class VisualizationPanel extends BorderPane {
} }
}); });
setCenter(notificationPane); 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); //configure visualization mode toggle
HBox.setHgrow(rightSeperator, Priority.ALWAYS); visualizationModeLabel.setText(Bundle.VisualizationPanel_visualizationModeLabel_text());
ChangeListener<Toggle> toggleListener = (ObservableValue<? extends Toggle> observable, countsToggle.setText(Bundle.VisualizationPanel_countsToggle_text());
Toggle oldValue, detailsToggle.setText(Bundle.VisualizationPanel_detailsToggle_text());
Toggle newValue) -> { ChangeListener<Toggle> toggleListener = (ObservableValue<? extends Toggle> observable, Toggle oldValue, Toggle newValue) -> {
if (newValue == null) { if (newValue == null) {
countsToggle.getToggleGroup().selectToggle(oldValue != null ? oldValue : countsToggle); countsToggle.getToggleGroup().selectToggle(oldValue != null ? oldValue : countsToggle);
} else if (newValue == countsToggle && oldValue != null) { } else if (newValue == countsToggle && oldValue != null) {
@ -258,14 +292,34 @@ final public class VisualizationPanel extends BorderPane {
countsToggle.getToggleGroup().selectedToggleProperty().addListener(toggleListener); countsToggle.getToggleGroup().selectedToggleProperty().addListener(toggleListener);
}); });
} }
countsToggle.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.countsToggle.text")); // NON-NLS controller.viewModeProperty().addListener(observable -> setViewMode(controller.viewModeProperty().get()));
detailsToggle.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.detailsToggle.text")); // NON-NLS 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 //setup rangeslider
rangeSlider.setOpacity(.7); rangeSlider.setOpacity(.7);
rangeSlider.setMin(0); rangeSlider.setMin(0);
rangeSlider.setBlockIncrement(1); rangeSlider.setBlockIncrement(1);
rangeHistogramStack.getChildren().add(rangeSlider); 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 histogramBox.setStyle(" -fx-padding: 0,0.5em,0,.5em; "); // NON-NLS
//configure zoom buttons
zoomMenuButton.getItems().clear(); zoomMenuButton.getItems().clear();
for (ZoomRanges b : ZoomRanges.values()) { for (ZoomRanges zoomRange : ZoomRanges.values()) {
MenuItem menuItem = new MenuItem(b.getDisplayName()); zoomMenuButton.getItems().add(ActionUtils.createMenuItem(
menuItem.setOnAction((event) -> { new Action(zoomRange.getDisplayName(), event -> {
if (b != ZoomRanges.ALL) { if (zoomRange != ZoomRanges.ALL) {
controller.pushPeriod(b.getPeriod()); controller.pushPeriod(zoomRange.getPeriod());
} else { } else {
controller.showFullRange(); controller.showFullRange();
} }
}); })));
zoomMenuButton.getItems().add(menuItem);
} }
zoomMenuButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomMenuButton.text")); // NON-NLS zoomMenuButton.setText(Bundle.VisualizationPanel_zoomMenuButton_text());
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());
ActionUtils.configureButton(new ZoomOut(controller), zoomOutButton); ActionUtils.configureButton(new ZoomOut(controller), zoomOutButton);
ActionUtils.configureButton(new ZoomIn(controller), zoomInButton); ActionUtils.configureButton(new ZoomIn(controller), zoomInButton);
setViewMode(controller.viewModeProperty().get()); //register for EventBus events (tags)
controller.getNeedsHistogramRebuild().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> { 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) { if (newValue) {
refreshHistorgram(); refreshHistorgram();
} }
}); });
controller.viewModeProperty().addListener((ObservableValue<? extends VisualizationMode> ov, VisualizationMode t, VisualizationMode t1) -> {
setViewMode(t1);
});
TimeLineController.getTimeZone().addListener(timeRangeInvalidationListener);
refreshHistorgram(); refreshHistorgram();
} }
private void setViewMode(VisualizationMode visualizationMode) { private void setViewMode(VisualizationMode visualizationMode) {
@ -382,19 +419,19 @@ final public class VisualizationPanel extends BorderPane {
TagsFilter tagsFilter = filteredEvents.getFilter().getTagsFilter(); TagsFilter tagsFilter = filteredEvents.getFilter().getTagsFilter();
if (tagsFilter.isSelected() && tagsFilter.isDisabled() == false) { if (tagsFilter.isSelected() && tagsFilter.isDisabled() == false) {
Platform.runLater(() -> { Platform.runLater(() -> {
notificationPane.show(VisualizationPanel_tagsAddedOrDeleted(), new ImageView(INFORMATION)); notificationPane.show(Bundle.VisualizationPanel_tagsAddedOrDeleted(), new ImageView(INFORMATION));
}); });
} }
} }
synchronized private void refreshHistorgram() { synchronized private void refreshHistorgram() {
if (histogramTask != null) { if (histogramTask != null) {
histogramTask.cancel(true); histogramTask.cancel(true);
} }
histogramTask = new LoggedTask<Void>( histogramTask = new LoggedTask<Void>(
NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.title"), true) { // NON-NLS NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.title"), true) { // NON-NLS
private final Lighting lighting = new Lighting();
@Override @Override
protected Void call() throws Exception { protected Void call() throws Exception {
@ -471,18 +508,10 @@ final public class VisualizationPanel extends BorderPane {
controller.monitorTask(histogramTask); controller.monitorTask(histogramTask);
} }
private InvalidationListener timeRangeInvalidationListener = (Observable observable) -> {
refreshTimeUI(filteredEvents.timeRangeProperty().get());
};
private InvalidationListener zoomListener = (Observable observable) -> {
notificationPane.hide();
};
private void refreshTimeUI(Interval interval) { private void refreshTimeUI(Interval interval) {
RangeDivisionInfo rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()); RangeDivisionInfo rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval());
final Long minTime = rangeDivisionInfo.getLowerBound(); final long minTime = rangeDivisionInfo.getLowerBound();
final long maxTime = rangeDivisionInfo.getUpperBound(); final long maxTime = rangeDivisionInfo.getUpperBound();
if (minTime > 0 && maxTime > minTime) { if (minTime > 0 && maxTime > minTime) {
@ -493,11 +522,13 @@ final public class VisualizationPanel extends BorderPane {
rangeSlider.highValueChangingProperty().removeListener(rangeSliderListener); rangeSlider.highValueChangingProperty().removeListener(rangeSliderListener);
rangeSlider.lowValueChangingProperty().removeListener(rangeSliderListener); rangeSlider.lowValueChangingProperty().removeListener(rangeSliderListener);
rangeSlider.setMax((Long) (maxTime - minTime)); rangeSlider.setMax((maxTime - minTime));
rangeSlider.setHighValue(interval.getEndMillis() - minTime); long startMillis = interval.getStartMillis();
rangeSlider.setLowValue(interval.getStartMillis() - minTime); long endMillis = interval.getEndMillis();
endPicker.setLocalDateTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(interval.getEndMillis()), TimeLineController.getTimeZoneID())); rangeSlider.setLowValue(startMillis - minTime);
startPicker.setLocalDateTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(interval.getStartMillis()), TimeLineController.getTimeZoneID())); rangeSlider.setHighValue(endMillis - minTime);
startPicker.setLocalDateTime(epochMillisToLocalDateTime(startMillis));
endPicker.setLocalDateTime(epochMillisToLocalDateTime(endMillis));
rangeSlider.highValueChangingProperty().addListener(rangeSliderListener); rangeSlider.highValueChangingProperty().addListener(rangeSliderListener);
rangeSlider.lowValueChangingProperty().addListener(rangeSliderListener); rangeSlider.lowValueChangingProperty().addListener(rangeSliderListener);
@ -509,26 +540,17 @@ final public class VisualizationPanel extends BorderPane {
private class NoEventsDialog extends TitledPane { 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 @FXML
private Button resetFiltersButton; private Button resetFiltersButton;
@FXML @FXML
private Button dismissButton; private Button dismissButton;
@FXML @FXML
private Button zoomButton; private Button zoomButton;
@FXML @FXML
private Label noEventsDialogLabel; private Label noEventsDialogLabel;
private final Runnable closeCallback;
private NoEventsDialog(Runnable closeCallback) { private NoEventsDialog(Runnable closeCallback) {
this.closeCallback = closeCallback; this.closeCallback = closeCallback;
FXMLConstructor.construct(this, "NoEventsDialog.fxml"); // NON-NLS 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 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<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(() -> refreshTimeUI(filteredEvents.timeRangeProperty().get()));
}
}
}
/**
* 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;
}
}
}
} }