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;
48 import org.openide.windows.WindowManager;
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;
68 private final TimeLineController controller;
69 private final FilteredEventsModel filteredEvents;
71 private IntervalSelector<? extends String> intervalSelector;
73 final ObservableList<Node> selectedNodes;
80 private RangeDivisionInfo rangeInfo;
82 EventCountsChart(TimeLineController controller, CategoryAxis dateAxis, NumberAxis countAxis, ObservableList<Node> selectedNodes) {
83 super(dateAxis, countAxis);
84 this.controller = controller;
85 this.filteredEvents = controller.getEventsModel();
88 dateAxis.setAnimated(
true);
89 dateAxis.setLabel(null);
90 dateAxis.setTickLabelsVisible(
false);
91 dateAxis.setTickLabelGap(0);
93 countAxis.setAutoRanging(
false);
94 countAxis.setLowerBound(0);
95 countAxis.setAnimated(
true);
96 countAxis.setMinorTickCount(0);
97 countAxis.setTickLabelFormatter(
new IntegerOnlyStringConverter());
99 setAlternativeRowFillVisible(
true);
101 setLegendVisible(
false);
105 ChartDragHandler<String, EventCountsChart> chartDragHandler =
new ChartDragHandler<>(
this);
106 setOnMousePressed(chartDragHandler);
107 setOnMouseReleased(chartDragHandler);
108 setOnMouseDragged(chartDragHandler);
110 setOnMouseClicked(
new MouseClickedHandler<>(
this));
112 this.selectedNodes = selectedNodes;
114 getController().getEventsModel().timeRangeProperty().addListener(o -> {
115 clearIntervalSelector();
120 public void clearContextMenu() {
121 chartContextMenu = null;
125 public ContextMenu getContextMenu(MouseEvent clickEvent) {
126 if (chartContextMenu != null) {
127 chartContextMenu.hide();
130 chartContextMenu = ActionUtils.createContextMenu(
131 Arrays.asList(TimeLineChart.newZoomHistoyActionGroup(controller)));
132 chartContextMenu.setAutoHide(
true);
133 return chartContextMenu;
137 public TimeLineController getController() {
142 public void clearIntervalSelector() {
143 getChartChildren().remove(intervalSelector);
144 intervalSelector = null;
148 public IntervalSelector<? extends String> getIntervalSelector() {
149 return intervalSelector;
154 intervalSelector = newIntervalSelector;
156 intervalSelector.prefHeightProperty().addListener(observable -> newIntervalSelector.autosize());
157 getChartChildren().add(getIntervalSelector());
161 public CountsIntervalSelector newIntervalSelector() {
162 return new CountsIntervalSelector(
this);
166 public ObservableList<Node> getSelectedNodes() {
167 return selectedNodes;
170 void setRangeInfo(RangeDivisionInfo rangeInfo) {
171 this.rangeInfo = rangeInfo;
174 Effect getSelectionEffect() {
175 return SELECTED_NODE_EFFECT;
188 "# {1} - event type displayname",
189 "# {2} - start date time",
190 "# {3} - end date time",
191 "CountsViewPane.tooltip.text={0} {1} events\nbetween {2}\nand {3}"})
193 protected void dataItemAdded(Series<String, Number> series,
int itemIndex, Data<String, Number> item) {
194 ExtraData extraValue = (ExtraData) item.getExtraValue();
195 EventType eventType = extraValue.getEventType();
196 Interval interval = extraValue.getInterval();
197 long count = extraValue.getRawCount();
199 item.nodeProperty().addListener((Observable o) -> {
200 final Node node = item.getNode();
202 node.setStyle(
"-fx-border-width: 2; -fx-border-color: " + ColorUtilities.getRGBCode(eventType.getSuperType().getColor()) +
"; -fx-bar-fill: " + ColorUtilities.getRGBCode(eventType.getColor()));
203 node.setCursor(Cursor.HAND);
205 final Tooltip tooltip =
new Tooltip(Bundle.CountsViewPane_tooltip_text(
206 count, eventType.getDisplayName(),
208 interval.getEnd().toString(rangeInfo.getTickFormatter())));
209 tooltip.setGraphic(
new ImageView(eventType.getFXImage()));
210 Tooltip.install(node, tooltip);
212 node.setOnMouseEntered(mouseEntered -> node.setEffect(
new DropShadow(10, eventType.getColor())));
213 node.setOnMouseExited(mouseExited -> node.setEffect(selectedNodes.contains(node) ? SELECTED_NODE_EFFECT : null));
214 node.setOnMouseClicked(
new BarClickHandler(item));
217 super.dataItemAdded(series, itemIndex, item);
228 return n.intValue() == n.doubleValue()
229 ? Integer.toString(n.intValue()) :
"";
235 return Double.valueOf(
string).intValue();
249 this.countsChart =
chart;
266 return new Interval(lowerDate, upperDate.plus(countsChart.rangeInfo.getPeriodSize().getPeriod()));
271 return date == null ?
new DateTime(countsChart.rangeInfo.getLowerBound()) : countsChart.rangeInfo.getTickFormatter().parseDateTime(date);
298 EventCountsChart.ExtraData extraData = (EventCountsChart.ExtraData) data.getExtraValue();
299 this.interval = extraData.getInterval();
300 this.type = extraData.getEventType();
301 this.node = data.getNode();
302 this.startDateString = data.getXValue();
305 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectTimeRange=Select Time Range"})
306 class SelectIntervalAction extends Action {
308 SelectIntervalAction() {
309 super(Bundle.Timeline_ui_countsview_menuItem_selectTimeRange());
310 setEventHandler(action -> {
312 selectedNodes.clear();
313 for (XYChart.Series<String, Number> s : getData()) {
314 s.getData().forEach((XYChart.Data<String, Number> d) -> {
315 if (startDateString.contains(d.getXValue())) {
316 selectedNodes.add(d.getNode());
324 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectEventType=Select Event Type"})
325 class SelectTypeAction extends Action {
328 super(Bundle.Timeline_ui_countsview_menuItem_selectEventType());
329 setEventHandler(action -> {
330 controller.selectTimeAndType(filteredEvents.getSpanningInterval(), type);
331 selectedNodes.clear();
332 getData().stream().filter(series -> series.getName().equals(type.
getDisplayName()))
334 .ifPresent(series -> series.getData().forEach(data -> selectedNodes.add(data.getNode())));
339 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.selectTimeandType=Select Time and Type"})
340 class SelectIntervalAndTypeAction extends Action {
342 SelectIntervalAndTypeAction() {
343 super(Bundle.Timeline_ui_countsview_menuItem_selectTimeandType());
344 setEventHandler(action -> {
345 controller.selectTimeAndType(interval, type);
346 selectedNodes.setAll(node);
351 @NbBundle.Messages({
"Timeline.ui.countsview.menuItem.zoomIntoTimeRange=Zoom into Time Range"})
352 class ZoomToIntervalAction extends Action {
354 ZoomToIntervalAction() {
355 super(Bundle.Timeline_ui_countsview_menuItem_zoomIntoTimeRange());
356 setEventHandler(action -> {
357 if (interval.toDuration().isShorterThan(Seconds.ONE.toStandardDuration()) ==
false) {
358 controller.pushTimeRange(interval);
366 "CountsViewPane.detailSwitchMessage=There is no temporal resolution smaller than Seconds.\nWould you like to switch to the Details view instead?",
367 "CountsViewPane.detailSwitchTitle=\"Switch to Details View?"})
370 if (e.getClickCount() == 1) {
371 if (e.getButton().equals(MouseButton.PRIMARY)) {
372 controller.selectTimeAndType(interval, type);
373 selectedNodes.setAll(node);
374 }
else if (e.getButton().equals(MouseButton.SECONDARY)) {
375 getContextMenu(e).hide();
377 if (barContextMenu == null) {
378 barContextMenu =
new ContextMenu();
379 barContextMenu.setAutoHide(
true);
380 barContextMenu.getItems().addAll(
381 ActionUtils.createMenuItem(
new SelectIntervalAction()),
382 ActionUtils.createMenuItem(
new SelectTypeAction()),
383 ActionUtils.createMenuItem(
new SelectIntervalAndTypeAction()),
384 new SeparatorMenuItem(),
385 ActionUtils.createMenuItem(
new ZoomToIntervalAction()));
387 barContextMenu.getItems().addAll(getContextMenu(e).getItems());
390 barContextMenu.show(node, e.getScreenX(), e.getScreenY());
393 }
else if (e.getClickCount() >= 2) {
394 if (interval.toDuration().isLongerThan(Seconds.ONE.toStandardDuration())) {
395 controller.pushTimeRange(interval);
398 int showConfirmDialog = JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(),
399 Bundle.CountsViewPane_detailSwitchMessage(),
400 Bundle.CountsViewPane_detailSwitchTitle(), JOptionPane.YES_NO_OPTION);
401 if (showConfirmDialog == JOptionPane.YES_OPTION) {
413 static class ExtraData {
415 private final Interval interval;
417 private final long rawCount;
419 ExtraData(Interval interval,
EventType eventType,
long rawCount) {
420 this.interval = interval;
421 this.eventType = eventType;
422 this.rawCount = rawCount;
425 public long getRawCount() {
429 public Interval getInterval() {
433 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)