\n
").append(outFolder.getName()).append("
\n"); // NON-NLS
- wrapper.append("
.append(outFolder.getName()).append(REPORT_IMAGE_EXTENSION)
"); // NON-NLS
+ wrapper.append("
\n
").append(reportDirectory.getName()).append("
\n"); // NON-NLS
+ wrapper.append("
.append(reportDirectory.getName()).append(REPORT_IMAGE_EXTENSION)
"); // NON-NLS
wrapper.append("
\n"); // NON-NLS
for (Pair pair : reportMetaData) {
wrapper.append("").append(pair.getKey()).append(": | ").append(pair.getValue()).append(" |
\n"); // NON-NLS
}
wrapper.append("
\n"); // NON-NLS
wrapper.append("
\n\n"); // NON-NLS
+ File reportHTMLFIle = new File(reportDirectory, reportName + HTML_EXT);
//write html wrapper
- try (Writer htmlWriter = new FileWriter(new File(outFolder, name + HTML_EXT))) {
+ try (Writer htmlWriter = new FileWriter(reportHTMLFIle)) {
htmlWriter.write(wrapper.toString());
} catch (FileNotFoundException ex) {
LOGGER.log(Level.WARNING, "failed to open html wrapper file for writing ", ex); // NON-NLS
@@ -121,14 +119,14 @@ public class SaveSnapshot extends Action {
//copy css
try (InputStream resource = this.getClass().getResourceAsStream("/org/sleuthkit/autopsy/timeline/index.css")) { // NON-NLS
- Files.copy(resource, Paths.get(outFolder.getPath(), "index.css")); // NON-NLS
+ Files.copy(resource, Paths.get(reportPath, "index.css")); // NON-NLS
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "failed to copy css file", ex); // NON-NLS
}
//add html file as report to case
try {
- Case.getCurrentCase().addReport(outFolder.getPath() + File.separator + outFolder.getName() + HTML_EXT, "Timeline", outFolder.getName() + HTML_EXT); // NON-NLS
+ Case.getCurrentCase().addReport(reportHTMLFIle.getPath(), "Timeline", reportName + HTML_EXT); // NON-NLS
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "failed add html wrapper as a report", ex); // NON-NLS
}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomIn.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomIn.java
new file mode 100644
index 0000000000..d1f76c3c7e
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomIn.java
@@ -0,0 +1,44 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015 Basis Technology Corp.
+ * Contact: carrier
sleuthkit 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.actions;
+
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import org.controlsfx.control.action.Action;
+import org.openide.util.NbBundle;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+
+/**
+ *
+ */
+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.",
+ "ZoomIn.action.text=Zoom in"})
+ public ZoomIn(TimeLineController controller) {
+ super(Bundle.ZoomIn_action_text());
+ setLongText(Bundle.ZoomIn_longText());
+ setGraphic(new ImageView(MAGNIFIER_IN));
+ setEventHandler(actionEvent -> {
+ controller.pushZoomInTime();
+ });
+ }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java
index ee362dfc40..8818ff4557 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2014 Basis Technology Corp.
+ * Copyright 2015 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,7 +19,8 @@
package org.sleuthkit.autopsy.timeline.actions;
import javafx.beans.binding.BooleanBinding;
-import javafx.event.ActionEvent;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
import org.controlsfx.control.action.Action;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.timeline.TimeLineController;
@@ -30,15 +31,22 @@ import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
*/
public class ZoomOut extends Action {
- private final TimeLineController controller;
+ private static final Image MAGNIFIER_OUT = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png"); //NOI18N
- private final FilteredEventsModel eventsModel;
+ @NbBundle.Messages({"ZoomOut.longText=Zoom out to view 50% more time.",
+ "ZoomOut.action.text=Zoom out"})
+ public ZoomOut(TimeLineController controller) {
+ super(Bundle.ZoomOut_action_text());
+ setLongText(Bundle.ZoomOut_longText());
+ setGraphic(new ImageView(MAGNIFIER_OUT));
+ setEventHandler(actionEvent -> {
+ controller.pushZoomOutTime();
+ });
- public ZoomOut(final TimeLineController controller) {
- super(NbBundle.getMessage(ZoomOut.class, "ZoomOut.action.name.text"));
- this.controller = controller;
- eventsModel = controller.getEventsModel();
+ //disable action when the current time range already encompases the entire case.
disabledProperty().bind(new BooleanBinding() {
+ private final FilteredEventsModel eventsModel = controller.getEventsModel();
+
{
bind(eventsModel.zoomParametersProperty());
}
@@ -48,8 +56,5 @@ public class ZoomOut extends Action {
return eventsModel.zoomParametersProperty().getValue().getTimeRange().contains(eventsModel.getSpanningInterval());
}
});
- setEventHandler((ActionEvent t) -> {
- controller.zoomOutToActivity();
- });
}
}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomToEvents.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomToEvents.java
new file mode 100644
index 0000000000..7bcc076936
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ZoomToEvents.java
@@ -0,0 +1,61 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-15 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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.actions;
+
+import javafx.beans.binding.BooleanBinding;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import org.controlsfx.control.action.Action;
+import org.openide.util.NbBundle;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
+
+/**
+ *
+ */
+public class ZoomToEvents extends Action {
+
+ private static final Image MAGNIFIER_OUT = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png", 16, 16, true, true); //NOI18N
+
+ @NbBundle.Messages({"ZoomToEvents.action.text=Zoom to events",
+ "ZoomToEvents.longText=Zoom out to show the nearest events."})
+ public ZoomToEvents(final TimeLineController controller) {
+ super(Bundle.ZoomToEvents_action_text());
+ setLongText(Bundle.ZoomToEvents_longText());
+ setGraphic(new ImageView(MAGNIFIER_OUT));
+ setEventHandler(actionEvent -> {
+ controller.zoomOutToActivity();
+ });
+
+ //disable action when the current time range already encompases the entire case.
+ disabledProperty().bind(new BooleanBinding() {
+ private final FilteredEventsModel eventsModel = controller.getEventsModel();
+
+ {
+ bind(eventsModel.zoomParametersProperty());
+ }
+
+ @Override
+ protected boolean computeValue() {
+ //TODO: do a db query to see if using this action will actually result in viewable events
+ return eventsModel.zoomParametersProperty().getValue().getTimeRange().contains(eventsModel.getSpanningInterval());
+ }
+ });
+ }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java
index ae354f87f8..f34c46fc64 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java
@@ -41,7 +41,6 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent.DeletedContentTagInfo;
import org.sleuthkit.autopsy.coreutils.Logger;
-import org.sleuthkit.autopsy.timeline.TimeLineView;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType;
import org.sleuthkit.autopsy.timeline.db.EventsRepository;
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/cross-script.png b/Core/src/org/sleuthkit/autopsy/timeline/images/cross-script.png
new file mode 100644
index 0000000000..f37cf41830
Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/cross-script.png differ
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/information-gray.png b/Core/src/org/sleuthkit/autopsy/timeline/images/information-gray.png
new file mode 100644
index 0000000000..c0dd006c9f
Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/information-gray.png differ
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/select.png b/Core/src/org/sleuthkit/autopsy/timeline/images/select.png
new file mode 100644
index 0000000000..a8c5583a7f
Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/select.png differ
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java
similarity index 88%
rename from Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java
rename to Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java
index 993f61501e..8d16c7a86f 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java
@@ -40,6 +40,7 @@ import javafx.scene.chart.Chart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
+import javafx.scene.control.Tooltip;
import javafx.scene.effect.Effect;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
@@ -50,12 +51,11 @@ import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
-import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import org.apache.commons.lang3.StringUtils;
+import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.TimeLineController;
-import org.sleuthkit.autopsy.timeline.TimeLineView;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent;
@@ -73,8 +73,15 @@ import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent;
* {@link XYChart} doing the rendering. Is this a good idea? -jm TODO: pull up
* common history context menu items out of derived classes? -jm
*/
-public abstract class AbstractVisualization & TimeLineChart> extends BorderPane implements TimeLineView {
+public abstract class AbstractVisualizationPane & TimeLineChart> extends BorderPane {
+ @NbBundle.Messages("AbstractVisualization.Drag_Tooltip.text=Drag the mouse to select a time interval to zoom into.")
+ private static final Tooltip DRAG_TOOLTIP = new Tooltip(Bundle.AbstractVisualization_Drag_Tooltip_text());
+ private static final Logger LOGGER = Logger.getLogger(AbstractVisualizationPane.class.getName());
+
+ public static Tooltip getDragTooltip() {
+ return DRAG_TOOLTIP;
+ }
protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true);
protected final ObservableList> dataSets = FXCollections.>observableArrayList();
@@ -93,11 +100,15 @@ public abstract class AbstractVisualization & T
*/
private Task updateTask;
- protected TimeLineController controller;
+ final protected TimeLineController controller;
- protected FilteredEventsModel filteredEvents;
+ final protected FilteredEventsModel filteredEvents;
- protected ReadOnlyListWrapper selectedNodes = new ReadOnlyListWrapper<>(FXCollections.observableArrayList());
+ final protected ReadOnlyListWrapper selectedNodes = new ReadOnlyListWrapper<>(FXCollections.observableArrayList());
+
+ private InvalidationListener invalidationListener = (Observable observable) -> {
+ update();
+ };
public ReadOnlyListProperty getSelectedNodes() {
return selectedNodes.getReadOnlyProperty();
@@ -177,7 +188,7 @@ public abstract class AbstractVisualization & T
* Primarily this invokes the background {@link Task} returned by
* {@link #getUpdateTask()} which derived classes must implement.
*/
- synchronized public void update() {
+ final synchronized public void update() {
if (updateTask != null) {
updateTask.cancel(true);
updateTask = null;
@@ -195,7 +206,7 @@ public abstract class AbstractVisualization & T
try {
this.hasEvents.set(updateTask.get());
} catch (InterruptedException | ExecutionException ex) {
- Logger.getLogger(AbstractVisualization.class.getName()).log(Level.SEVERE, "Unexpected exception updating visualization", ex);
+ LOGGER.log(Level.SEVERE, "Unexpected exception updating visualization", ex); //NOI18N
}
break;
}
@@ -203,7 +214,7 @@ public abstract class AbstractVisualization & T
controller.monitorTask(updateTask);
}
- synchronized public void dispose() {
+ final synchronized public void dispose() {
if (updateTask != null) {
updateTask.cancel(true);
}
@@ -211,7 +222,12 @@ public abstract class AbstractVisualization & T
invalidationListener = null;
}
- protected AbstractVisualization(Pane partPane, Pane contextPane, Region spacer) {
+ protected AbstractVisualizationPane(TimeLineController controller, Pane partPane, Pane contextPane, Region spacer) {
+ this.controller = controller;
+
+ this.filteredEvents = controller.getEventsModel();
+ this.filteredEvents.registerForEvents(this);
+ this.filteredEvents.zoomParametersProperty().addListener(invalidationListener);
this.leafPane = partPane;
this.branchPane = contextPane;
this.spacer = spacer;
@@ -226,32 +242,18 @@ public abstract class AbstractVisualization & T
});
}
});
- }
- @Override
- synchronized public void setController(TimeLineController controller) {
- this.controller = controller;
- chart.setController(controller);
-
- setModel(controller.getEventsModel());
- TimeLineController.getTimeZone().addListener((Observable observable) -> {
- update();
+ TimeLineController.getTimeZone().addListener(invalidationListener);
+
+ //show tooltip text in status bar
+ hoverProperty().addListener((observable, oldActivated, newActivated) -> {
+ if (newActivated) {
+ controller.setStatus(DRAG_TOOLTIP.getText());
+ } else {
+ controller.setStatus("");
+ }
});
- }
-
- @Override
- synchronized public void setModel(@Nonnull FilteredEventsModel filteredEvents) {
-
- if (this.filteredEvents != null && this.filteredEvents != filteredEvents) {
- this.filteredEvents.unRegisterForEvents(this);
- this.filteredEvents.zoomParametersProperty().removeListener(invalidationListener);
- }
- if (this.filteredEvents != filteredEvents) {
- filteredEvents.registerForEvents(this);
- filteredEvents.zoomParametersProperty().addListener(invalidationListener);
- }
- this.filteredEvents = filteredEvents;
-
+
update();
}
@@ -260,10 +262,6 @@ public abstract class AbstractVisualization & T
update();
}
- protected InvalidationListener invalidationListener = (Observable observable) -> {
- update();
- };
-
/**
* iterate through the list of tick-marks building a two level structure of
* replacement tick marl labels. (Visually) upper level has most
@@ -370,7 +368,7 @@ public abstract class AbstractVisualization & T
*/
private synchronized void assignLeafLabel(String labelText, double labelWidth, double labelX, boolean bold) {
- Text label = new Text(" " + labelText + " ");
+ Text label = new Text(" " + labelText + " "); //NOI18N
label.setTextAlignment(TextAlignment.CENTER);
label.setFont(Font.font(null, bold ? FontWeight.BOLD : FontWeight.NORMAL, 10));
//position label accounting for width
@@ -414,9 +412,9 @@ public abstract class AbstractVisualization & T
label.relocate(labelX, 0);
if (labelX == 0) { // first label has no border
- label.setStyle("-fx-border-width: 0 0 0 0 ; -fx-border-color:black;"); // NON-NLS
+ label.setStyle("-fx-border-width: 0 0 0 0 ; -fx-border-color:black;"); // NON-NLS //NOI18N
} else { // subsequent labels have border on left to create dividers
- label.setStyle("-fx-border-width: 0 0 0 1; -fx-border-color:black;"); // NON-NLS
+ label.setStyle("-fx-border-width: 0 0 0 1; -fx-border-color:black;"); // NON-NLS //NOI18N
}
branchPane.getChildren().add(label);
@@ -446,10 +444,10 @@ public abstract class AbstractVisualization & T
TwoPartDateTime(String dateString) {
//find index of separator to spit on
- int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":");
+ int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":"); //NOI18N
if (splitIndex < 0) { // there is only one part
leaf = dateString;
- branch = "";
+ branch = ""; //NOI18N
} else { //split at index
leaf = StringUtils.substring(dateString, splitIndex + 1);
branch = StringUtils.substring(dateString, 0, splitIndex);
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties
index 1c31801521..521cf76e4f 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties
+++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013 Basis Technology Corp.
+ * Copyright 2013-15 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -35,7 +35,6 @@ Timeline.ui.ZoomRanges.threeyears.text=Three Years
Timeline.ui.ZoomRanges.fiveyears.text=Five Years
Timeline.ui.ZoomRanges.tenyears.text=Ten Years
Timeline.ui.ZoomRanges.all.text=All
-Timeline.ui.TimeLineChart.tooltip.text=Double-click to zoom into range\:\n{0} to {1}\nRight-click to clear.
TimeLineResultView.startDateToEndDate.text={0} to {1}
VisualizationPanel.histogramTask.title=Rebuild Histogram
VisualizationPanel.histogramTask.preparing=preparing
@@ -56,4 +55,4 @@ 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
\ No newline at end of file
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.css b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.css
new file mode 100644
index 0000000000..b349466632
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.css
@@ -0,0 +1,16 @@
+.intervalSelector{
+ -fx-background-color: rgba(0,0,255,.25);
+ -fx-border-color: rgba(0,0,255,.25);
+ -fx-border-width: 0 3 0 3;
+}
+
+.closeButton{
+}
+
+.closeButton:hover{
+ -fx-opacity: 1;
+}
+
+.zoomButton:hover{
+ -fx-opacity: 1;
+}
\ No newline at end of file
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml
new file mode 100644
index 0000000000..a3885619d4
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java
new file mode 100644
index 0000000000..197914167a
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java
@@ -0,0 +1,316 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-15 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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 javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.geometry.Point2D;
+import javafx.geometry.Pos;
+import javafx.scene.Cursor;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.Tooltip;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.BorderPane;
+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.timeline.FXMLConstructor;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+
+/**
+ * Visually represents a 'selected' time range, and allows mouse interactions
+ * with it.
+ *
+ * @param the type of values along the x axis this is a selector for
+ *
+ * This abstract class requires concrete implementations to implement template
+ * methods to handle formating and date 'lookup' of the generic x-axis type
+ */
+public abstract class IntervalSelector extends BorderPane {
+
+ private static final Image ClEAR_INTERVAL_ICON = new Image("/org/sleuthkit/autopsy/timeline/images/cross-script.png", 16, 16, true, true, true);
+ private static final Image ZOOM_TO_INTERVAL_ICON = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png", 16, 16, true, true, true);
+ private static final double STROKE_WIDTH = 3;
+ private static final double HALF_STROKE = STROKE_WIDTH / 2;
+
+ /**
+ * the Axis this is a selector over
+ */
+ public final TimeLineChart chart;
+
+ private Tooltip tooltip;
+ /////////drag state
+ private DragPosition dragPosition;
+ private double startLeft;
+ private double startDragX;
+ private double startWidth;
+
+ private final BooleanProperty isDragging = new SimpleBooleanProperty(false);
+ /////////end drag state
+ private final TimeLineController controller;
+
+ @FXML
+ private Label startLabel;
+
+ @FXML
+ private Label endLabel;
+
+ @FXML
+ private Button closeButton;
+
+ @FXML
+ private Button zoomButton;
+
+ @FXML
+ private BorderPane bottomBorder;
+
+ public IntervalSelector(TimeLineChart chart) {
+ this.chart = chart;
+ this.controller = chart.getController();
+ FXMLConstructor.construct(this, IntervalSelector.class, "IntervalSelector.fxml"); // NON-NLS
+ }
+
+ @FXML
+ void initialize() {
+ assert startLabel != null : "fx:id=\"startLabel\" was not injected: check your FXML file 'IntervalSelector.fxml'.";
+ assert endLabel != null : "fx:id=\"endLabel\" was not injected: check your FXML file 'IntervalSelector.fxml'.";
+ assert closeButton != null : "fx:id=\"closeButton\" was not injected: check your FXML file 'IntervalSelector.fxml'.";
+ assert zoomButton != null : "fx:id=\"zoomButton\" was not injected: check your FXML file 'IntervalSelector.fxml'.";
+
+ setMaxHeight(USE_PREF_SIZE);
+ setMinHeight(USE_PREF_SIZE);
+ setMaxWidth(USE_PREF_SIZE);
+ setMinWidth(USE_PREF_SIZE);
+
+ BooleanBinding showingControls = hoverProperty().and(isDragging.not());
+ closeButton.visibleProperty().bind(showingControls);
+ closeButton.managedProperty().bind(showingControls);
+ zoomButton.visibleProperty().bind(showingControls);
+ zoomButton.managedProperty().bind(showingControls);
+
+ widthProperty().addListener(o -> {
+ IntervalSelector.this.updateStartAndEnd();
+ if (startLabel.getWidth() + zoomButton.getWidth() + endLabel.getWidth() > getWidth()) {
+ this.setCenter(zoomButton);
+ } else {
+ bottomBorder.setCenter(zoomButton);
+ }
+ BorderPane.setAlignment(zoomButton, Pos.BOTTOM_CENTER);
+ });
+ layoutXProperty().addListener(o -> this.updateStartAndEnd());
+ updateStartAndEnd();
+
+ setOnMouseMoved(mouseMove -> {
+ Point2D parentMouse = getLocalMouseCoords(mouseMove);
+ final double diffX = getLayoutX() - parentMouse.getX();
+ if (Math.abs(diffX) <= HALF_STROKE) {
+ setCursor(Cursor.W_RESIZE);
+ } else if (Math.abs(diffX + getWidth()) <= HALF_STROKE) {
+ setCursor(Cursor.E_RESIZE);
+ } else {
+ setCursor(Cursor.HAND);
+ }
+ mouseMove.consume();
+ });
+
+ setOnMousePressed(mousePress -> {
+ Point2D parentMouse = getLocalMouseCoords(mousePress);
+ final double diffX = getLayoutX() - parentMouse.getX();
+ startDragX = mousePress.getScreenX();
+ startWidth = getWidth();
+ startLeft = getLayoutX();
+ if (Math.abs(diffX) <= HALF_STROKE) {
+ dragPosition = IntervalSelector.DragPosition.LEFT;
+ } else if (Math.abs(diffX + getWidth()) <= HALF_STROKE) {
+ dragPosition = IntervalSelector.DragPosition.RIGHT;
+ } else {
+ dragPosition = IntervalSelector.DragPosition.CENTER;
+ }
+ mousePress.consume();
+ });
+
+ setOnMouseReleased(mouseRelease -> isDragging.set(false));
+ setOnMouseDragged(mouseDrag -> {
+ isDragging.set(true);
+ double dX = mouseDrag.getScreenX() - startDragX;
+ switch (dragPosition) {
+ case CENTER:
+ setLayoutX(startLeft + dX);
+ break;
+ case LEFT:
+ if (dX > startWidth) {
+ startDragX = mouseDrag.getScreenX();
+ startWidth = 0;
+ dragPosition = DragPosition.RIGHT;
+ } else {
+ setLayoutX(startLeft + dX);
+ setPrefWidth(startWidth - dX);
+ autosize();
+ }
+ break;
+ case RIGHT:
+ Point2D parentMouse = getLocalMouseCoords(mouseDrag);
+ if (parentMouse.getX() < startLeft) {
+ dragPosition = DragPosition.LEFT;
+ startDragX = mouseDrag.getScreenX();
+ startWidth = 0;
+ } else {
+ setPrefWidth(startWidth + dX);
+ autosize();
+ }
+ break;
+ }
+ mouseDrag.consume();
+ });
+
+ ActionUtils.configureButton(new ZoomToSelectedIntervalAction(), zoomButton);
+ ActionUtils.configureButton(new ClearSelectedIntervalAction(), closeButton);
+
+ //have to add handler rather than use convenience methods so that charts can listen for dismisal click
+ setOnMouseClicked(mosueClick -> {
+ if (mosueClick.getButton() == MouseButton.SECONDARY) {
+ chart.clearIntervalSelector();
+ mosueClick.consume();
+ }
+ if (mosueClick.getClickCount() >= 2) {
+ zoomToSelectedInterval();
+ mosueClick.consume();
+ }
+ });
+ }
+
+ private Point2D getLocalMouseCoords(MouseEvent mouseEvent) {
+ return getParent().sceneToLocal(new Point2D(mouseEvent.getSceneX(), mouseEvent.getSceneY()));
+ }
+
+ private void zoomToSelectedInterval() {
+ //convert to DateTimes, using max/min if null(off axis)
+ DateTime start = parseDateTime(getSpanStart());
+ DateTime end = parseDateTime(getSpanEnd());
+ Interval i = adjustInterval(start.isBefore(end) ? new Interval(start, end) : new Interval(end, start));
+ controller.pushTimeRange(i);
+ }
+
+ /**
+ *
+ * @param i the interval represented by this selector
+ *
+ * @return a modified version of {@code i} adjusted to suite the needs of
+ * the concrete implementation
+ */
+ protected abstract Interval adjustInterval(Interval i);
+
+ /**
+ * format a string representation of the given x-axis value to use in the
+ * tooltip
+ *
+ * @param date a x-axis value of type X
+ *
+ * @return a string representation of the given x-axis value
+ */
+ protected abstract String formatSpan(final X date);
+
+ /**
+ * parse an x-axis value to a {@link DateTime}
+ *
+ * @param date a x-axis value of type X
+ *
+ * @return a {@link DateTime} corresponding to the given x-axis value
+ */
+ protected abstract DateTime parseDateTime(X date);
+
+ @NbBundle.Messages(value = {"# {0} - start timestamp",
+ "# {1} - end timestamp",
+ "Timeline.ui.TimeLineChart.tooltip.text=Double-click to zoom into range:\n{0} to {1}\nRight-click to clear."})
+ private void updateStartAndEnd() {
+ String startString = formatSpan(getSpanStart());
+ String endString = formatSpan(getSpanEnd());
+ startLabel.setText(startString);
+ endLabel.setText(endString);
+
+ Tooltip.uninstall(this, tooltip);
+ tooltip = new Tooltip(Bundle.Timeline_ui_TimeLineChart_tooltip_text(startString, endString));
+ Tooltip.install(this, tooltip);
+ }
+
+ /**
+ * @return the value along the x-axis corresponding to the left edge of the
+ * selector
+ */
+ public X getSpanEnd() {
+ return getValueForDisplay(getBoundsInParent().getMaxX());
+ }
+
+ /**
+ * @return the value along the x-axis corresponding to the right edge of the
+ * selector
+ */
+ public X getSpanStart() {
+ return getValueForDisplay(getBoundsInParent().getMinX());
+ }
+
+ private X getValueForDisplay(final double display) {
+ return chart.getXAxis().getValueForDisplay(chart.getXAxis().parentToLocal(display, 0).getX());
+ }
+
+ /**
+ * enum to represent whether the drag is a left/right-edge modification or a
+ * horizontal slide triggered by dragging the center
+ */
+ private enum DragPosition {
+
+ LEFT,
+ CENTER,
+ RIGHT
+ }
+
+ private class ZoomToSelectedIntervalAction extends Action {
+
+ @NbBundle.Messages("IntervalSelector.ZoomAction.name=Zoom")
+ ZoomToSelectedIntervalAction() {
+ super(Bundle.IntervalSelector_ZoomAction_name());
+ setGraphic(new ImageView(ZOOM_TO_INTERVAL_ICON));
+ setEventHandler((ActionEvent t) -> {
+ zoomToSelectedInterval();
+ });
+ }
+ }
+
+ private class ClearSelectedIntervalAction extends Action {
+
+ @NbBundle.Messages("IntervalSelector.ClearSelectedIntervalAction.tooltTipText=Clear Selected Interval")
+ ClearSelectedIntervalAction() {
+ super("");
+ setLongText(Bundle.IntervalSelector_ClearSelectedIntervalAction_tooltTipText());
+ setGraphic(new ImageView(ClEAR_INTERVAL_ICON));
+ setEventHandler((ActionEvent t) -> {
+ chart.clearIntervalSelector();
+ });
+ }
+ }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml
index 781bb18d1a..a731fb13a6 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml
+++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml
@@ -5,7 +5,7 @@
-
+
+