19 package org.sleuthkit.autopsy.timeline.ui.countsview;
 
   21 import java.util.Arrays;
 
   22 import java.util.Optional;
 
   23 import java.util.logging.Level;
 
   24 import javafx.collections.ObservableList;
 
   25 import javafx.event.EventHandler;
 
   26 import javafx.scene.Cursor;
 
   27 import javafx.scene.Node;
 
   28 import javafx.scene.chart.CategoryAxis;
 
   29 import javafx.scene.chart.NumberAxis;
 
   30 import javafx.scene.chart.StackedBarChart;
 
   31 import javafx.scene.chart.XYChart;
 
   32 import javafx.scene.control.Alert;
 
   33 import javafx.scene.control.ButtonType;
 
   34 import javafx.scene.control.ContextMenu;
 
   35 import javafx.scene.control.SeparatorMenuItem;
 
   36 import javafx.scene.control.Tooltip;
 
   37 import javafx.scene.effect.DropShadow;
 
   38 import javafx.scene.effect.Effect;
 
   39 import javafx.scene.effect.Lighting;
 
   40 import javafx.scene.image.ImageView;
 
   41 import javafx.scene.input.MouseButton;
 
   42 import javafx.scene.input.MouseEvent;
 
   43 import javafx.util.StringConverter;
 
   44 import org.controlsfx.control.Notifications;
 
   45 import org.controlsfx.control.action.Action;
 
   46 import org.controlsfx.control.action.ActionUtils;
 
   47 import org.joda.time.DateTime;
 
   48 import org.joda.time.Interval;
 
   49 import org.joda.time.Seconds;
 
   50 import org.openide.util.NbBundle;
 
   69 final class EventCountsChart 
extends StackedBarChart<String, Number> implements TimeLineChart<String> {
 
   71     private static final Logger logger = Logger.getLogger(EventCountsChart.class.getName());
 
   72     private static final Effect SELECTED_NODE_EFFECT = 
new Lighting();
 
   73     private ContextMenu chartContextMenu;
 
   75     private final TimeLineController controller;
 
   76     private final EventsModel filteredEvents;
 
   78     private IntervalSelector<? extends String> intervalSelector;
 
   80     final ObservableList<Node> selectedNodes;
 
   87     private RangeDivision rangeInfo;
 
   89     EventCountsChart(TimeLineController controller, CategoryAxis dateAxis, NumberAxis countAxis, ObservableList<Node> selectedNodes) {
 
   90         super(dateAxis, countAxis);
 
   91         this.controller = controller;
 
   92         this.filteredEvents = controller.getEventsModel();
 
   95         dateAxis.setAnimated(
true);
 
   96         dateAxis.setLabel(null);
 
   97         dateAxis.setTickLabelsVisible(
false);
 
   98         dateAxis.setTickLabelGap(0);
 
  100         countAxis.setAutoRanging(
false);
 
  101         countAxis.setLowerBound(0);
 
  102         countAxis.setAnimated(
true);
 
  103         countAxis.setMinorTickCount(0);
 
  104         countAxis.setTickLabelFormatter(
new IntegerOnlyStringConverter());
 
  106         setAlternativeRowFillVisible(
true);
 
  108         setLegendVisible(
false);
 
  112         ChartDragHandler<String, EventCountsChart> chartDragHandler = 
new ChartDragHandler<>(
this);
 
  113         setOnMousePressed(chartDragHandler);
 
  114         setOnMouseReleased(chartDragHandler);
 
  115         setOnMouseDragged(chartDragHandler);
 
  117         setOnMouseClicked(
new MouseClickedHandler<>(
this));
 
  119         this.selectedNodes = selectedNodes;
 
  121         getController().getEventsModel().timeRangeProperty().addListener(o -> {
 
  122             clearIntervalSelector();
 
  127     public void clearContextMenu() {
 
  128         chartContextMenu = null;
 
  132     public ContextMenu getContextMenu(MouseEvent clickEvent) {
 
  133         if (chartContextMenu != null) {
 
  134             chartContextMenu.hide();
 
  137         chartContextMenu = ActionUtils.createContextMenu(
 
  138                 Arrays.asList(TimeLineChart.newZoomHistoyActionGroup(controller)));
 
  139         chartContextMenu.setAutoHide(
true);
 
  140         return chartContextMenu;
 
  144     public TimeLineController getController() {
 
  149     public void clearIntervalSelector() {
 
  150         getChartChildren().remove(intervalSelector);
 
  151         intervalSelector = null;
 
  155     public IntervalSelector<? extends String> getIntervalSelector() {
 
  156         return intervalSelector;
 
  161         intervalSelector = newIntervalSelector;
 
  163         intervalSelector.prefHeightProperty().addListener(observable -> newIntervalSelector.autosize());
 
  164         getChartChildren().add(getIntervalSelector());
 
  168     public CountsIntervalSelector newIntervalSelector() {
 
  169         return new CountsIntervalSelector(
this);
 
  173     public ObservableList<Node> getSelectedNodes() {
 
  174         return selectedNodes;
 
  177     void setRangeInfo(RangeDivision rangeInfo) {
 
  178         this.rangeInfo = rangeInfo;
 
  181     Effect getSelectionEffect() {
 
  182         return SELECTED_NODE_EFFECT;
 
  195         "# {1} - event type displayname",
 
  196         "# {2} - start date time",
 
  197         "# {3} - end date time",
 
  198         "CountsViewPane.tooltip.text={0} {1} events\nbetween {2}\nand     {3}"})
 
  200     protected void dataItemAdded(Series<String, Number> series, 
int itemIndex, Data<String, Number> item) {
 
  201         ExtraData extraValue = (ExtraData) item.getExtraValue();
 
  202         TimelineEventType eventType = extraValue.getEventType();
 
  203         Interval interval = extraValue.getInterval();
 
  204         long count = extraValue.getRawCount();
 
  206         item.nodeProperty().addListener(observable -> {
 
  207             final Node node = item.getNode();
 
  209                 node.setStyle(
"-fx-border-width: 2; " 
  210                               + 
" -fx-border-color: " + ColorUtilities.getRGBCode(getColor(eventType.getParent())) + 
"; " 
  211                               + 
" -fx-bar-fill: " + ColorUtilities.getRGBCode(getColor(eventType))); 
 
  212                 node.setCursor(Cursor.HAND);
 
  214                 final Tooltip tooltip = 
new Tooltip(Bundle.CountsViewPane_tooltip_text(
 
  215                         count, eventType.getDisplayName(),
 
  217                         interval.getEnd().toString(rangeInfo.getTickFormatter())));
 
  218                 tooltip.setGraphic(
new ImageView(getImagePath(eventType)));
 
  219                 Tooltip.install(node, tooltip);
 
  221                 node.setOnMouseEntered(mouseEntered -> node.setEffect(
new DropShadow(10, getColor(eventType))));
 
  222                 node.setOnMouseExited(mouseExited -> node.setEffect(selectedNodes.contains(node) ? SELECTED_NODE_EFFECT : null));
 
  223                 node.setOnMouseClicked(
new BarClickHandler(item));
 
  226         super.dataItemAdded(series, itemIndex, item); 
 
  237             return n.intValue() == n.doubleValue()
 
  238                     ? Integer.toString(n.intValue()) : 
"";
 
  244             return Double.valueOf(
string).intValue();
 
  258             this.countsChart = 
chart;
 
  275             return new Interval(lowerDate, upperDate.plus(countsChart.rangeInfo.getPeriodSize().toUnitPeriod()));
 
  280             return date == null ? 
new DateTime(countsChart.rangeInfo.getLowerBound()) : countsChart.rangeInfo.getTickFormatter().parseDateTime(date);
 
  299         private final TimelineEventType 
type;
 
  306             EventCountsChart.ExtraData extraData = (EventCountsChart.ExtraData) data.getExtraValue();
 
  307             this.interval = extraData.getInterval();
 
  308             this.type = extraData.getEventType();
 
  309             this.node = data.getNode();
 
  310             this.startDateString = data.getXValue();
 
  313         @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectTimeRange=Select Time Range",
 
  314             "SelectIntervalAction.errorMessage=Error selecting interval."})
 
  315         class SelectIntervalAction extends Action {
 
  317             SelectIntervalAction() {
 
  318                 super(Bundle.Timeline_ui_countsview_menuItem_selectTimeRange());
 
  319                 setEventHandler(action -> {
 
  321                         controller.selectTimeAndType(interval, TimelineEventType.ROOT_EVENT_TYPE);
 
  323                     } 
catch (TskCoreException ex) {
 
  324                         Notifications.create().owner(getScene().getWindow())
 
  325                                 .text(Bundle.SelectIntervalAction_errorMessage())
 
  327                         logger.log(Level.SEVERE, 
"Error selecting interval.", ex);
 
  329                     selectedNodes.clear();
 
  330                     for (XYChart.Series<String, Number> s : getData()) {
 
  331                         s.getData().forEach((XYChart.Data<String, Number> d) -> {
 
  332                             if (startDateString.contains(d.getXValue())) {
 
  333                                 selectedNodes.add(d.getNode());
 
  341         @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectEventType=Select Event Type",
 
  342             "SelectTypeAction.errorMessage=Error selecting type."})
 
  343         class SelectTypeAction extends Action {
 
  346                 super(Bundle.Timeline_ui_countsview_menuItem_selectEventType());
 
  347                 setEventHandler(action -> {
 
  349                         controller.selectTimeAndType(filteredEvents.getSpanningInterval(), type);
 
  351                     } 
catch (TskCoreException ex) {
 
  352                         Notifications.create().owner(getScene().getWindow())
 
  353                                 .text(Bundle.SelectTypeAction_errorMessage())
 
  355                         logger.log(Level.SEVERE, 
"Error selecting type.", ex);
 
  357                     selectedNodes.clear();
 
  358                     getData().stream().filter(series -> series.getName().equals(type.getDisplayName()))
 
  360                             .ifPresent(series -> series.getData().forEach(data -> selectedNodes.add(data.getNode())));
 
  365         @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectTimeandType=Select Time and Type",
 
  366             "SelectIntervalAndTypeAction.errorMessage=Error selecting interval and type."})
 
  367         class SelectIntervalAndTypeAction extends Action {
 
  369             SelectIntervalAndTypeAction() {
 
  370                 super(Bundle.Timeline_ui_countsview_menuItem_selectTimeandType());
 
  371                 setEventHandler(action -> {
 
  373                         controller.selectTimeAndType(interval, type);
 
  375                     } 
catch (TskCoreException ex) {
 
  376                         Notifications.create().owner(getScene().getWindow())
 
  377                                 .text(Bundle.SelectIntervalAndTypeAction_errorMessage())
 
  379                         logger.log(Level.SEVERE, 
"Error selecting interval and type.", ex);
 
  381                     selectedNodes.setAll(node);
 
  386         @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.zoomIntoTimeRange=Zoom into Time Range",
 
  387             "ZoomToIntervalAction.errorMessage=Error zooming to interval."})
 
  388         class ZoomToIntervalAction extends Action {
 
  390             ZoomToIntervalAction() {
 
  391                 super(Bundle.Timeline_ui_countsview_menuItem_zoomIntoTimeRange());
 
  392                 setEventHandler(action -> {
 
  394                         if (interval.toDuration().isShorterThan(Seconds.ONE.toStandardDuration()) == 
false) {
 
  395                             controller.pushTimeRange(interval);
 
  397                     } 
catch (TskCoreException ex) {
 
  398                         Notifications.create().owner(getScene().getWindow())
 
  399                                 .text(Bundle.ZoomToIntervalAction_errorMessage())
 
  401                         logger.log(Level.SEVERE, 
"Error zooming to interval.", ex);
 
  409             "CountsViewPane.detailSwitchMessage=There is no temporal resolution smaller than Seconds.\nWould you like to switch to the Details view instead?",
 
  410             "CountsViewPane.detailSwitchTitle=\"Switch to Details View?",
 
  411             "BarClickHandler.selectTimeAndType.errorMessage=Error selecting time and type.",
 
  412             "BarClickHandler_zoomIn_errorMessage=Error zooming in."})
 
  415             if (e.getClickCount() == 1) {     
 
  416                 if (e.getButton().equals(MouseButton.PRIMARY)) {
 
  418                         controller.selectTimeAndType(interval, type);
 
  419                     } 
catch (TskCoreException ex) {
 
  420                         Notifications.create().owner(getScene().getWindow())
 
  421                                 .text(Bundle.BarClickHandler_selectTimeAndType_errorMessage())
 
  423                         logger.log(Level.SEVERE, 
"Error selecting time and type.", ex);
 
  425                     selectedNodes.setAll(node);
 
  426                 } 
else if (e.getButton().equals(MouseButton.SECONDARY)) {
 
  427                     getContextMenu(e).hide();
 
  429                     if (barContextMenu == null) {
 
  430                         barContextMenu = 
new ContextMenu();
 
  431                         barContextMenu.setAutoHide(
true);
 
  432                         barContextMenu.getItems().addAll(
 
  433                                 ActionUtils.createMenuItem(
new SelectIntervalAction()),
 
  434                                 ActionUtils.createMenuItem(
new SelectTypeAction()),
 
  435                                 ActionUtils.createMenuItem(
new SelectIntervalAndTypeAction()),
 
  436                                 new SeparatorMenuItem(),
 
  437                                 ActionUtils.createMenuItem(
new ZoomToIntervalAction()));
 
  439                         barContextMenu.getItems().addAll(getContextMenu(e).getItems());
 
  442                     barContextMenu.show(node, e.getScreenX(), e.getScreenY());
 
  445             } 
else if (e.getClickCount() >= 2) {  
 
  446                 if (interval.toDuration().isLongerThan(Seconds.ONE.toStandardDuration())) {
 
  448                         controller.pushTimeRange(interval);
 
  449                     } 
catch (TskCoreException ex) {
 
  450                         Notifications.create().owner(getScene().getWindow())
 
  451                                 .text(Bundle.BarClickHandler_zoomIn_errorMessage())
 
  453                         logger.log(Level.SEVERE, 
"Error zooming in.", ex);
 
  456                     Alert alert = 
new Alert(Alert.AlertType.CONFIRMATION, Bundle.CountsViewPane_detailSwitchMessage(), ButtonType.YES, ButtonType.NO);
 
  457                     alert.setTitle(Bundle.CountsViewPane_detailSwitchTitle());
 
  460                     alert.showAndWait().ifPresent(response -> {
 
  461                         if (response == ButtonType.YES) {
 
  462                             controller.setViewMode(ViewMode.DETAIL);
 
  474     static class ExtraData {
 
  476         private final Interval interval;
 
  477         private final TimelineEventType eventType;
 
  478         private final long rawCount;
 
  480         ExtraData(Interval interval, TimelineEventType eventType, 
long rawCount) {
 
  481             this.interval = interval;
 
  482             this.eventType = eventType;
 
  483             this.rawCount = rawCount;
 
  486         public long getRawCount() {
 
  490         public Interval getInterval() {
 
  494         public TimelineEventType getEventType() {
 
Number fromString(String string)
String formatSpan(String date)
Interval adjustInterval(Interval i)
final EventCountsChart countsChart
String toString(Number n)
final TimelineEventType type
static DateTimeZone getJodaTimeZone()
DateTime parseDateTime(String date)
final IntervalSelectorProvider< X > chart
static void setDialogIcons(Dialog<?> dialog)
static String getImagePath(TimelineEventType type)
ContextMenu barContextMenu
final String startDateString
static RangeDivision getRangeDivision(Interval timeRange, DateTimeZone timeZone)
static Color getColor(TimelineEventType type)
void setIntervalSelector(IntervalSelector<?extends X > newIntervalSelector)
void handle(final MouseEvent e)