/*
* Autopsy Forensic Browser
*
* Copyright 2014-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.ui;
import 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.chart.Axis;
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 javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
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 <X> 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<X> extends BorderPane {
private static final Image CLEAR_INTERVAL_ICON = new Image("/org/sleuthkit/autopsy/timeline/images/cross-script.png", 16, 16, true, true, true); //NON-NLS
private static final Image ZOOM_TO_INTERVAL_ICON = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png", 16, 16, true, true, true); //NON-NLS
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 IntervalSelectorProvider<X> 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(IntervalSelectorProvider<X> 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 = zoomButton.hoverProperty().or(bottomBorder.hoverProperty().or(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() - 10) {
this.setCenter(zoomButton);
bottomBorder.setCenter(new Rectangle(10, 10, Color.TRANSPARENT));
} 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((MouseEvent mouseRelease) -> {
isDragging.set(false);
mouseRelease.consume();;
});
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();
});
setOnMouseClicked(mouseClick -> {
if (mouseClick.getButton() == MouseButton.SECONDARY) {
chart.clearIntervalSelector();
} else if (mouseClick.getClickCount() >= 2) {
zoomToSelectedInterval();
mouseClick.consume();
}
});
ActionUtils.configureButton(new ZoomToSelectedIntervalAction(), zoomButton);
ActionUtils.configureButton(new ClearSelectedIntervalAction(), closeButton);
}
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 DateTime
*
* @param date a x-axis value of type X
*
* @return a 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}.\n\nRight-click to close."})
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();
});
}
}
public interface IntervalSelectorProvider<X> {
public TimeLineController getController();
IntervalSelector<? extends X> getIntervalSelector();
void setIntervalSelector(IntervalSelector<? extends X> newIntervalSelector);
/**
* derived classes should implement this so as to supply an appropriate
* subclass of {@link IntervalSelector}
*
* @return a new interval selector
*/
IntervalSelector<X> newIntervalSelector();
/**
* Clear any references to previous interval selectors , including
* removing the interval selector from the UI / scene-graph.
*/
void clearIntervalSelector();
public Axis<X> getXAxis();
}
}