19 package org.sleuthkit.autopsy.timeline.ui.countsview;
21 import java.util.Arrays;
22 import javafx.beans.Observable;
23 import javafx.collections.ObservableList;
24 import javafx.event.EventHandler;
25 import javafx.scene.Cursor;
26 import javafx.scene.Node;
27 import javafx.scene.chart.CategoryAxis;
28 import javafx.scene.chart.NumberAxis;
29 import javafx.scene.chart.StackedBarChart;
30 import javafx.scene.chart.XYChart;
31 import javafx.scene.control.ContextMenu;
32 import javafx.scene.control.SeparatorMenuItem;
33 import javafx.scene.control.Tooltip;
34 import javafx.scene.effect.DropShadow;
35 import javafx.scene.effect.Effect;
36 import javafx.scene.effect.Lighting;
37 import javafx.scene.image.ImageView;
38 import javafx.scene.input.MouseButton;
39 import javafx.scene.input.MouseEvent;
40 import javafx.util.StringConverter;
41 import javax.swing.JOptionPane;
42 import org.controlsfx.control.action.Action;
43 import org.controlsfx.control.action.ActionUtils;
44 import org.joda.time.DateTime;
45 import org.joda.time.Interval;
46 import org.joda.time.Seconds;
47 import org.openide.util.NbBundle;
62 final class EventCountsChart
extends StackedBarChart<String, Number> implements TimeLineChart<String> {
64 private static final Effect SELECTED_NODE_EFFECT =
new Lighting();
65 private ContextMenu chartContextMenu;
67 private final TimeLineController controller;
68 private final FilteredEventsModel filteredEvents;
70 private IntervalSelector<? extends String> intervalSelector;
72 final ObservableList<Node> selectedNodes;
79 private RangeDivisionInfo rangeInfo;
81 EventCountsChart(TimeLineController controller, CategoryAxis dateAxis, NumberAxis countAxis, ObservableList<Node> selectedNodes) {
82 super(dateAxis, countAxis);
83 this.controller = controller;
84 this.filteredEvents = controller.getEventsModel();
87 dateAxis.setAnimated(
true);
88 dateAxis.setLabel(null);
89 dateAxis.setTickLabelsVisible(
false);
90 dateAxis.setTickLabelGap(0);
92 countAxis.setAutoRanging(
false);
93 countAxis.setLowerBound(0);
94 countAxis.setAnimated(
true);
95 countAxis.setMinorTickCount(0);
96 countAxis.setTickLabelFormatter(
new IntegerOnlyStringConverter());
98 setAlternativeRowFillVisible(
true);
100 setLegendVisible(
false);
104 ChartDragHandler<String, EventCountsChart> chartDragHandler =
new ChartDragHandler<>(
this);
105 setOnMousePressed(chartDragHandler);
106 setOnMouseReleased(chartDragHandler);
107 setOnMouseDragged(chartDragHandler);
109 setOnMouseClicked(
new MouseClickedHandler<>(
this));
111 this.selectedNodes = selectedNodes;
113 getController().getEventsModel().timeRangeProperty().addListener(o -> {
114 clearIntervalSelector();
119 public void clearContextMenu() {
120 chartContextMenu = null;
124 public ContextMenu getContextMenu(MouseEvent clickEvent) {
125 if (chartContextMenu != null) {
126 chartContextMenu.hide();
129 chartContextMenu = ActionUtils.createContextMenu(
130 Arrays.asList(TimeLineChart.newZoomHistoyActionGroup(controller)));
131 chartContextMenu.setAutoHide(
true);
132 return chartContextMenu;
136 public TimeLineController getController() {
141 public void clearIntervalSelector() {
142 getChartChildren().remove(intervalSelector);
143 intervalSelector = null;
147 public IntervalSelector<? extends String> getIntervalSelector() {
148 return intervalSelector;
153 intervalSelector = newIntervalSelector;
155 intervalSelector.prefHeightProperty().addListener(observable -> newIntervalSelector.autosize());
156 getChartChildren().add(getIntervalSelector());
160 public CountsIntervalSelector newIntervalSelector() {
161 return new CountsIntervalSelector(
this);
165 public ObservableList<Node> getSelectedNodes() {
166 return selectedNodes;
169 void setRangeInfo(RangeDivisionInfo rangeInfo) {
170 this.rangeInfo = rangeInfo;
173 Effect getSelectionEffect() {
174 return SELECTED_NODE_EFFECT;
187 "# {1} - event type displayname",
188 "# {2} - start date time",
189 "# {3} - end date time",
190 "CountsViewPane.tooltip.text={0} {1} events\nbetween {2}\nand {3}"})
192 protected void dataItemAdded(Series<String, Number> series,
int itemIndex, Data<String, Number> item) {
193 ExtraData extraValue = (ExtraData) item.getExtraValue();
194 EventType eventType = extraValue.getEventType();
195 Interval interval = extraValue.getInterval();
196 long count = extraValue.getRawCount();
198 item.nodeProperty().addListener((Observable o) -> {
199 final Node node = item.getNode();
201 node.setStyle(
"-fx-border-width: 2; -fx-border-color: " + ColorUtilities.getRGBCode(eventType.getSuperType().getColor()) +
"; -fx-bar-fill: " + ColorUtilities.getRGBCode(eventType.getColor()));
202 node.setCursor(Cursor.HAND);
204 final Tooltip tooltip =
new Tooltip(Bundle.CountsViewPane_tooltip_text(
205 count, eventType.getDisplayName(),
207 interval.getEnd().toString(rangeInfo.getTickFormatter())));
208 tooltip.setGraphic(
new ImageView(eventType.getFXImage()));
209 Tooltip.install(node, tooltip);
211 node.setOnMouseEntered(mouseEntered -> node.setEffect(
new DropShadow(10, eventType.getColor())));
212 node.setOnMouseExited(mouseExited -> node.setEffect(selectedNodes.contains(node) ? SELECTED_NODE_EFFECT : null));
213 node.setOnMouseClicked(
new BarClickHandler(item));
216 super.dataItemAdded(series, itemIndex, item);
227 return n.intValue() == n.doubleValue()
228 ? Integer.toString(n.intValue()) :
"";
234 return Double.valueOf(
string).intValue();
248 this.countsChart =
chart;
265 return new Interval(lowerDate, upperDate.plus(countsChart.rangeInfo.getPeriodSize().getPeriod()));
270 return date == null ?
new DateTime(countsChart.rangeInfo.getLowerBound()) : countsChart.rangeInfo.getTickFormatter().parseDateTime(date);
297 EventCountsChart.ExtraData extraData = (EventCountsChart.ExtraData) data.getExtraValue();
298 this.interval = extraData.getInterval();
299 this.type = extraData.getEventType();
300 this.node = data.getNode();
301 this.startDateString = data.getXValue();
304 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectTimeRange=Select Time Range"})
305 class SelectIntervalAction extends Action {
307 SelectIntervalAction() {
308 super(Bundle.Timeline_ui_countsview_menuItem_selectTimeRange());
309 setEventHandler(action -> {
311 selectedNodes.clear();
312 for (XYChart.Series<String, Number> s : getData()) {
313 s.getData().forEach((XYChart.Data<String, Number> d) -> {
314 if (startDateString.contains(d.getXValue())) {
315 selectedNodes.add(d.getNode());
323 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectEventType=Select Event Type"})
324 class SelectTypeAction extends Action {
327 super(Bundle.Timeline_ui_countsview_menuItem_selectEventType());
328 setEventHandler(action -> {
329 controller.selectTimeAndType(filteredEvents.getSpanningInterval(), type);
330 selectedNodes.clear();
331 getData().stream().filter(series -> series.getName().equals(type.
getDisplayName()))
333 .ifPresent(series -> series.getData().forEach(data -> selectedNodes.add(data.getNode())));
338 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectTimeandType=Select Time and Type"})
339 class SelectIntervalAndTypeAction extends Action {
341 SelectIntervalAndTypeAction() {
342 super(Bundle.Timeline_ui_countsview_menuItem_selectTimeandType());
343 setEventHandler(action -> {
344 controller.selectTimeAndType(interval, type);
345 selectedNodes.setAll(node);
350 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.zoomIntoTimeRange=Zoom into Time Range"})
351 class ZoomToIntervalAction extends Action {
353 ZoomToIntervalAction() {
354 super(Bundle.Timeline_ui_countsview_menuItem_zoomIntoTimeRange());
355 setEventHandler(action -> {
356 if (interval.toDuration().isShorterThan(Seconds.ONE.toStandardDuration()) ==
false) {
357 controller.pushTimeRange(interval);
365 "CountsViewPane.detailSwitchMessage=There is no temporal resolution smaller than Seconds.\nWould you like to switch to the Details view instead?",
366 "CountsViewPane.detailSwitchTitle=\"Switch to Details View?"})
369 if (e.getClickCount() == 1) {
370 if (e.getButton().equals(MouseButton.PRIMARY)) {
371 controller.selectTimeAndType(interval, type);
372 selectedNodes.setAll(node);
373 }
else if (e.getButton().equals(MouseButton.SECONDARY)) {
374 getContextMenu(e).hide();
376 if (barContextMenu == null) {
377 barContextMenu =
new ContextMenu();
378 barContextMenu.setAutoHide(
true);
379 barContextMenu.getItems().addAll(
380 ActionUtils.createMenuItem(
new SelectIntervalAction()),
381 ActionUtils.createMenuItem(
new SelectTypeAction()),
382 ActionUtils.createMenuItem(
new SelectIntervalAndTypeAction()),
383 new SeparatorMenuItem(),
384 ActionUtils.createMenuItem(
new ZoomToIntervalAction()));
386 barContextMenu.getItems().addAll(getContextMenu(e).getItems());
389 barContextMenu.show(node, e.getScreenX(), e.getScreenY());
392 }
else if (e.getClickCount() >= 2) {
393 if (interval.toDuration().isLongerThan(Seconds.ONE.toStandardDuration())) {
394 controller.pushTimeRange(interval);
397 int showConfirmDialog = JOptionPane.showConfirmDialog(null,
398 Bundle.CountsViewPane_detailSwitchMessage(),
399 Bundle.CountsViewPane_detailSwitchTitle(), JOptionPane.YES_NO_OPTION);
400 if (showConfirmDialog == JOptionPane.YES_OPTION) {
412 static class ExtraData {
414 private final Interval interval;
416 private final long rawCount;
418 ExtraData(Interval interval,
EventType eventType,
long rawCount) {
419 this.interval = interval;
420 this.eventType = eventType;
421 this.rawCount = rawCount;
424 public long getRawCount() {
428 public Interval getInterval() {
432 public EventType getEventType() {
Number fromString(String string)
String formatSpan(String date)
static RootEventType getInstance()
static RangeDivisionInfo getRangeDivisionInfo(Interval timeRange)
Interval adjustInterval(Interval i)
final EventCountsChart countsChart
String toString(Number n)
static DateTimeZone getJodaTimeZone()
DateTime parseDateTime(String date)
final IntervalSelectorProvider< X > chart
ContextMenu barContextMenu
final String startDateString
void setIntervalSelector(IntervalSelector<?extends X > newIntervalSelector)
void handle(final MouseEvent e)