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)