19 package org.sleuthkit.autopsy.timeline.ui.countsview;
21 import java.util.Arrays;
22 import java.util.MissingResourceException;
23 import javafx.beans.Observable;
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.ContextMenu;
33 import javafx.scene.control.SeparatorMenuItem;
34 import javafx.scene.control.Tooltip;
35 import javafx.scene.effect.DropShadow;
36 import javafx.scene.effect.Effect;
37 import javafx.scene.effect.Lighting;
38 import javafx.scene.image.ImageView;
39 import javafx.scene.input.MouseButton;
40 import javafx.scene.input.MouseEvent;
41 import javafx.util.StringConverter;
42 import javax.swing.JOptionPane;
43 import org.controlsfx.control.action.Action;
44 import org.controlsfx.control.action.ActionUtils;
45 import org.joda.time.DateTime;
46 import org.joda.time.Interval;
47 import org.joda.time.Seconds;
48 import org.openide.util.NbBundle;
63 final class EventCountsChart
extends StackedBarChart<String, Number> implements TimeLineChart<String> {
65 private static final Effect SELECTED_NODE_EFFECT =
new Lighting();
66 private ContextMenu chartContextMenu;
69 public ContextMenu getChartContextMenu() {
70 return chartContextMenu;
73 private final TimeLineController controller;
74 private final FilteredEventsModel filteredEvents;
76 private IntervalSelector<? extends String> intervalSelector;
78 final ObservableList<Node> selectedNodes;
84 private RangeDivisionInfo rangeInfo;
86 EventCountsChart(TimeLineController controller, CategoryAxis dateAxis, NumberAxis countAxis, ObservableList<Node> selectedNodes) {
87 super(dateAxis, countAxis);
88 this.controller = controller;
89 this.filteredEvents = controller.getEventsModel();
91 dateAxis.setAnimated(
true);
92 dateAxis.setLabel(null);
93 dateAxis.setTickLabelsVisible(
false);
94 dateAxis.setTickLabelGap(0);
96 countAxis.setLabel(NbBundle.getMessage(CountsViewPane.class,
"CountsChartPane.numberOfEvents"));
97 countAxis.setAutoRanging(
false);
98 countAxis.setLowerBound(0);
99 countAxis.setAnimated(
true);
100 countAxis.setMinorTickCount(0);
101 countAxis.setTickLabelFormatter(
new IntegerOnlyStringConverter());
103 setAlternativeRowFillVisible(
true);
105 setLegendVisible(
false);
109 ChartDragHandler<String, EventCountsChart> chartDragHandler =
new ChartDragHandler<>(
this);
110 setOnMousePressed(chartDragHandler);
111 setOnMouseReleased(chartDragHandler);
112 setOnMouseDragged(chartDragHandler);
114 setOnMouseClicked(
new MouseClickedHandler<>(
this));
116 this.selectedNodes = selectedNodes;
120 public void clearIntervalSelector() {
121 getChartChildren().remove(intervalSelector);
122 intervalSelector = null;
126 public ContextMenu getChartContextMenu(MouseEvent clickEvent)
throws MissingResourceException {
127 if (chartContextMenu != null) {
128 chartContextMenu.hide();
131 chartContextMenu = ActionUtils.createContextMenu(
132 Arrays.asList(TimeLineChart.newZoomHistoyActionGroup(controller)));
133 chartContextMenu.setAutoHide(
true);
134 return chartContextMenu;
138 public TimeLineController getController() {
143 public IntervalSelector<? extends String> getIntervalSelector() {
144 return intervalSelector;
149 intervalSelector = newIntervalSelector;
150 getChartChildren().add(getIntervalSelector());
154 public CountsIntervalSelector newIntervalSelector() {
155 return new CountsIntervalSelector(
this);
164 ContextMenu getContextMenu() {
165 return chartContextMenu;
168 void setRangeInfo(RangeDivisionInfo rangeInfo) {
169 this.rangeInfo = rangeInfo;
172 Effect getSelectionEffect() {
173 return SELECTED_NODE_EFFECT;
186 "# {1} - event type displayname",
187 "# {2} - start date time",
188 "# {3} - end date time",
189 "CountsViewPane.tooltip.text={0} {1} events\nbetween {2}\nand {3}"})
191 protected void dataItemAdded(Series<String, Number> series,
int itemIndex, Data<String, Number> item) {
192 ExtraData extraValue = (ExtraData) item.getExtraValue();
193 EventType eventType = extraValue.getEventType();
194 Interval interval = extraValue.getInterval();
195 long count = extraValue.getRawCount();
197 item.nodeProperty().addListener((Observable o) -> {
198 final Node node = item.getNode();
200 node.setStyle(
"-fx-border-width: 2; -fx-border-color: " + ColorUtilities.getRGBCode(eventType.getSuperType().getColor()) +
"; -fx-bar-fill: " + ColorUtilities.getRGBCode(eventType.getColor()));
201 node.setCursor(Cursor.HAND);
203 final Tooltip tooltip =
new Tooltip(Bundle.CountsViewPane_tooltip_text(
204 count, eventType.getDisplayName(),
206 interval.getEnd().toString(rangeInfo.getTickFormatter())));
207 tooltip.setGraphic(
new ImageView(eventType.getFXImage()));
208 Tooltip.install(node, tooltip);
210 node.setOnMouseEntered((mouseEntered) -> node.setEffect(
new DropShadow(10, eventType.getColor())));
211 node.setOnMouseExited((MouseEvent mouseExited) -> node.setEffect(selectedNodes.contains(node) ? SELECTED_NODE_EFFECT : null));
212 node.setOnMouseClicked(
new BarClickHandler(item));
215 super.dataItemAdded(series, itemIndex, item);
226 return n.intValue() == n.doubleValue()
227 ? Integer.toString(n.intValue()) :
"";
233 return Double.valueOf(
string).intValue();
247 this.countsChart =
chart;
264 return new Interval(lowerDate, upperDate.plus(countsChart.rangeInfo.getPeriodSize().getPeriod()));
269 return date == null ?
new DateTime(countsChart.rangeInfo.getLowerBound()) : countsChart.rangeInfo.getTickFormatter().parseDateTime(date);
296 EventCountsChart.ExtraData extraData = (EventCountsChart.ExtraData) data.getExtraValue();
297 this.interval = extraData.getInterval();
298 this.type = extraData.getEventType();
299 this.node = data.getNode();
300 this.startDateString = data.getXValue();
303 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectTimeRange=Select Time Range"})
304 class SelectIntervalAction extends Action {
306 SelectIntervalAction() {
307 super(Bundle.Timeline_ui_countsview_menuItem_selectTimeRange());
308 setEventHandler(action -> {
310 selectedNodes.clear();
311 for (XYChart.Series<String, Number> s : getData()) {
312 s.getData().forEach((XYChart.Data<String, Number> d) -> {
313 if (startDateString.contains(d.getXValue())) {
314 selectedNodes.add(d.getNode());
322 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectEventType=Select Event Type"})
323 class SelectTypeAction extends Action {
326 super(Bundle.Timeline_ui_countsview_menuItem_selectEventType());
327 setEventHandler(action -> {
328 controller.selectTimeAndType(filteredEvents.getSpanningInterval(), type);
329 selectedNodes.clear();
330 getData().stream().filter(series -> series.getName().equals(type.
getDisplayName()))
332 .ifPresent(series -> series.getData().forEach(data -> selectedNodes.add(data.getNode())));
337 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectTimeandType=Select Time and Type"})
338 class SelectIntervalAndTypeAction extends Action {
340 SelectIntervalAndTypeAction() {
341 super(Bundle.Timeline_ui_countsview_menuItem_selectTimeandType());
342 setEventHandler(action -> {
343 controller.selectTimeAndType(interval, type);
344 selectedNodes.setAll(node);
349 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.zoomIntoTimeRange=Zoom into Time Range"})
350 class ZoomToIntervalAction extends Action {
352 ZoomToIntervalAction() {
353 super(Bundle.Timeline_ui_countsview_menuItem_zoomIntoTimeRange());
354 setEventHandler(action -> {
355 if (interval.toDuration().isShorterThan(Seconds.ONE.toStandardDuration()) ==
false) {
356 controller.pushTimeRange(interval);
364 "CountsViewPane.detailSwitchMessage=There is no temporal resolution smaller than Seconds.\nWould you like to switch to the Details view instead?",
365 "CountsViewPane.detailSwitchTitle=\"Switch to Details View?"})
368 if (e.getClickCount() == 1) {
369 if (e.getButton().equals(MouseButton.PRIMARY)) {
370 controller.selectTimeAndType(interval, type);
371 selectedNodes.setAll(node);
372 }
else if (e.getButton().equals(MouseButton.SECONDARY)) {
373 getChartContextMenu(e).hide();
375 if (barContextMenu == null) {
376 barContextMenu =
new ContextMenu();
377 barContextMenu.setAutoHide(
true);
378 barContextMenu.getItems().addAll(
379 ActionUtils.createMenuItem(
new SelectIntervalAction()),
380 ActionUtils.createMenuItem(
new SelectTypeAction()),
381 ActionUtils.createMenuItem(
new SelectIntervalAndTypeAction()),
382 new SeparatorMenuItem(),
383 ActionUtils.createMenuItem(
new ZoomToIntervalAction()));
385 barContextMenu.getItems().addAll(getChartContextMenu(e).getItems());
388 barContextMenu.show(node, e.getScreenX(), e.getScreenY());
391 }
else if (e.getClickCount() >= 2) {
392 if (interval.toDuration().isLongerThan(Seconds.ONE.toStandardDuration())) {
393 controller.pushTimeRange(interval);
396 int showConfirmDialog = JOptionPane.showConfirmDialog(null,
397 Bundle.CountsViewPane_detailSwitchMessage(),
398 Bundle.CountsViewPane_detailSwitchTitle(), JOptionPane.YES_NO_OPTION);
399 if (showConfirmDialog == JOptionPane.YES_OPTION) {
430 static class ExtraData {
432 private final Interval interval;
434 private final long rawCount;
436 ExtraData(Interval interval,
EventType eventType,
long rawCount) {
437 this.interval = interval;
438 this.eventType = eventType;
439 this.rawCount = rawCount;
442 public long getRawCount() {
446 public Interval getInterval() {
450 public EventType getEventType() {
Number fromString(String string)
String formatSpan(String date)
final TimeLineChart< X > chart
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)
ContextMenu barContextMenu
final String startDateString
void setIntervalSelector(IntervalSelector<?extends X > newIntervalSelector)
void handle(final MouseEvent e)