diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index 874252f50b..d240f473a8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,6 +33,7 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.geometry.Pos; import javafx.scene.Cursor; @@ -75,7 +76,7 @@ import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; * @param the type of data plotted along the y axis * @param the type of nodes used to represent data items * @param the type of the {@link XYChart} this class uses to - * plot the * data. + * plot the data. * * TODO: this is becoming (too?) closely tied to the notion that their is a * {@link XYChart} doing the rendering. Is this a good idea? -jm TODO: pull up @@ -97,16 +98,13 @@ public abstract class AbstractVisualizationPane> dataSeries = FXCollections.>observableArrayList(); - - abstract protected void resetData(); + protected final Map> eventTypeToSeriesMap = new HashMap<>(); protected ChartType chart; //// replacement axis label componenets private final Pane leafPane; // container for the leaf lables in the declutterd axis - private final Pane branchPane;// container for the branch lables in the declutterd axis - protected final Region spacer; /** @@ -148,7 +146,7 @@ public abstract class AbstractVisualizationPane getUpdateTask(); + abstract protected Task getUpdateTask(); /** * @return return the {@link Effect} applied to 'selected nodes' in this * visualization, or null if selection is visualized via another * mechanism */ - protected abstract Effect getSelectionEffect(); + abstract protected Effect getSelectionEffect(); /** * @param tickValue * * @return a String to use for a tick mark label given a tick value */ - protected abstract String getTickMarkLabel(X tickValue); + abstract protected String getTickMarkLabel(X tickValue); /** * the spacing (in pixels) between tick marks of the horizontal axis. This @@ -185,22 +183,27 @@ public abstract class AbstractVisualizationPane getXAxis(); + abstract protected Axis getXAxis(); /** * @return the vertical axis used by this Visualization's chart */ - protected abstract Axis getYAxis(); + abstract protected Axis getYAxis(); + + abstract protected void resetData(); /** * update this visualization based on current state of zoom / filters. - * Primarily this invokes the background {@link Task} returned by - * {@link #getUpdateTask()} which derived classes must implement. + * Primarily this invokes the background {@link VisualizationUpdateTask} + * returned by {@link #getUpdateTask()}, which derived classes must + * implement. + * + * TODO: replace this logic with a {@link Service} ? -jm */ final synchronized public void update() { if (updateTask != null) { @@ -236,8 +239,10 @@ public abstract class AbstractVisualizationPane series = new XYChart.Series<>(); series.setName(eventType.getDisplayName()); @@ -246,9 +251,19 @@ public abstract class AbstractVisualizationPane getSeries(final EventType et) { + return eventTypeToSeriesMap.get(et); + } + protected AbstractVisualizationPane(TimeLineController controller, Pane partPane, Pane contextPane, Region spacer) { this.controller = controller; - this.filteredEvents = controller.getEventsModel(); this.filteredEvents.registerForEvents(this); this.filteredEvents.zoomParametersProperty().addListener(invalidationListener); @@ -268,18 +283,10 @@ public abstract class AbstractVisualizationPane { - if (newActivated) { - controller.setStatus(DEFAULT_TOOLTIP.getText()); - } else { - controller.setStatus(""); - } - }); + hoverProperty().addListener(observable -> controller.setStatus(isHover() ? DEFAULT_TOOLTIP.getText() : "")); } - protected final Map> eventTypeToSeriesMap = new HashMap<>(); - @Subscribe public void handleRefreshRequested(RefreshRequestedEvent event) { update(); @@ -477,17 +484,8 @@ public abstract class AbstractVisualizationPane getSeries(final EventType et) { - return eventTypeToSeriesMap.get(et); + protected Interval getTimeRange() { + return filteredEvents.timeRangeProperty().get(); } abstract protected class VisualizationUpdateTask extends LoggedTask { @@ -496,30 +494,36 @@ public abstract class AbstractVisualizationPane { - installMaskerPane(); + MaskerPane maskerPane = new MaskerPane(); + maskerPane.textProperty().bind(messageProperty()); + maskerPane.progressProperty().bind(progressProperty()); + setCenter(new StackPane(chart, maskerPane)); setCursor(Cursor.WAIT); }); return true; } + /** + * updates the horisontal axis and removes the blocking progress + * indicator. Derived Tasks should be sure to call this as part of their + * succeeded() implementation. + */ @Override protected void succeeded() { super.succeeded(); @@ -532,7 +536,8 @@ public abstract class AbstractVisualizationPane intervals = rangeInfo.getIntervals(); - //clear old data, and reset ranges and series List categories = Lists.transform(intervals, rangeInfo::formatForTick); + + //clear old data, and reset ranges and series resetChart(categories); updateMessage(Bundle.CountsViewPane_loggedTask_updatingCounts()); int chartMax = 0; int numIntervals = intervals.size(); + /* + * for each interval query database for event counts and add to + * chart. + * + * Doing this in chunks might seem inefficient but it lets us reuse + * more cached results as the user navigates to overlapping viewws + * + * //TODO: implement similar chunked caching in DetailsView -jm + */ for (int i = 0; i < numIntervals; i++) { if (isCancelled()) { return null; } updateProgress(i, numIntervals); final Interval interval = intervals.get(i); - int maxPerInterval = 0; //used in total max tracking + int maxPerInterval = 0; //query for current interval Map eventCounts = filteredEvents.getEventCounts(interval); @@ -290,6 +300,7 @@ public class CountsViewPane extends AbstractVisualizationPane 0; + return chartMax > 0; // are there events } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java index 61cf9ca507..fee0522fa6 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java @@ -173,6 +173,48 @@ final class EventCountsChart extends StackedBarChart implements return SELECTED_NODE_EFFECT; } + /** + * Add the bar click handler,tooltip, border styling and hover effect to the + * node generated by StackedBarChart. + * + * @param series + * @param itemIndex + * @param item + */ + @NbBundle.Messages({ + "# {0} - count", + "# {1} - event type displayname", + "# {2} - start date time", + "# {3} - end date time", + "CountsViewPane.tooltip.text={0} {1} events\nbetween {2}\nand {3}"}) + @Override + protected void dataItemAdded(Series series, int itemIndex, Data item) { + ExtraData extraValue = (ExtraData) item.getExtraValue(); + EventType eventType = extraValue.getEventType(); + Interval interval = extraValue.getInterval(); + long count = extraValue.getRawCount(); + + item.nodeProperty().addListener((Observable o) -> { + final Node node = item.getNode(); + if (node != null) { + node.setStyle("-fx-border-width: 2; -fx-border-color: " + ColorUtilities.getRGBCode(eventType.getSuperType().getColor()) + "; -fx-bar-fill: " + ColorUtilities.getRGBCode(eventType.getColor())); // NON-NLS + node.setCursor(Cursor.HAND); + + final Tooltip tooltip = new Tooltip(Bundle.CountsViewPane_tooltip_text( + count, eventType.getDisplayName(), + item.getXValue(), + interval.getEnd().toString(rangeInfo.getTickFormatter()))); + tooltip.setGraphic(new ImageView(eventType.getFXImage())); + Tooltip.install(node, tooltip); + + node.setOnMouseEntered((mouseEntered) -> node.setEffect(new DropShadow(10, eventType.getColor()))); + node.setOnMouseExited((MouseEvent mouseExited) -> node.setEffect(selectedNodes.contains(node) ? SELECTED_NODE_EFFECT : null)); + node.setOnMouseClicked(new BarClickHandler(item)); + } + }); + super.dataItemAdded(series, itemIndex, item); //To change body of generated methods, choose Tools | Templates. + } + /** * StringConvereter used to 'format' vertical axis labels */ @@ -228,40 +270,6 @@ final class EventCountsChart extends StackedBarChart implements } } - @NbBundle.Messages({ - "# {0} - count", - "# {1} - event type displayname", - "# {2} - start date time", - "# {3} - end date time", - "CountsViewPane.tooltip.text={0} {1} events\nbetween {2}\nand {3}"}) - @Override - protected void dataItemAdded(Series series, int itemIndex, Data item) { - ExtraData extraValue = (ExtraData) item.getExtraValue(); - EventType eventType = extraValue.getEventType(); - Interval interval = extraValue.getInterval(); - long count = extraValue.getRawCount(); - - item.nodeProperty().addListener((Observable o) -> { - final Node node = item.getNode(); - if (node != null) { - node.setStyle("-fx-border-width: 2; -fx-border-color: " + ColorUtilities.getRGBCode(eventType.getSuperType().getColor()) + "; -fx-bar-fill: " + ColorUtilities.getRGBCode(eventType.getColor())); // NON-NLS - node.setCursor(Cursor.HAND); - - final Tooltip tooltip = new Tooltip(Bundle.CountsViewPane_tooltip_text( - count, eventType.getDisplayName(), - item.getXValue(), - interval.getEnd().toString(rangeInfo.getTickFormatter()))); - tooltip.setGraphic(new ImageView(eventType.getFXImage())); - Tooltip.install(node, tooltip); - - node.setOnMouseEntered((mouseEntered) -> node.setEffect(new DropShadow(10, eventType.getColor()))); - node.setOnMouseExited((MouseEvent mouseExited) -> node.setEffect(selectedNodes.contains(node) ? SELECTED_NODE_EFFECT : null)); - node.setOnMouseClicked(new BarClickHandler(item)); - } - }); - super.dataItemAdded(series, itemIndex, item); //To change body of generated methods, choose Tools | Templates. - } - /** * EventHandler for click events on nodes representing a bar(segment) in the * stacked bar chart. @@ -415,6 +423,10 @@ final class EventCountsChart extends StackedBarChart implements } } + /** + * Encapsulate extra data stuffed into each {@link Data} item to give click + * handler and tooltip access to more info. + */ static class ExtraData { private final Interval interval; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java index 387120f7ef..01876bcc14 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java @@ -110,7 +110,9 @@ public final class EventDetailsChart extends XYChart impl private ContextMenu chartContextMenu; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass private Set activeQuickHidefilters; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass private double descriptionWidth; @Override @@ -420,12 +422,15 @@ public final class EventDetailsChart extends XYChart impl protected void layoutPlotChildren() { setCursor(Cursor.WAIT); maxY.set(0); + + //These don't change during a layout pass and are expensive to compute per node. So we do it once at the start activeQuickHidefilters = getController().getQuickHideFilters().stream() .filter(AbstractFilter::isActive) .map(DescriptionFilter::getDescription) .collect(Collectors.toSet()); - descriptionWidth = getDescriptionWidth(); + //This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start + descriptionWidth = truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE; if (bandByType.get()) { sortedStripeNodes.stream() @@ -438,10 +443,6 @@ public final class EventDetailsChart extends XYChart impl setCursor(null); } - private double getDescriptionWidth() { - return truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE; - } - ReadOnlyDoubleProperty maxVScrollProperty() { return maxY.getReadOnlyProperty(); } @@ -531,8 +532,8 @@ public final class EventDetailsChart extends XYChart impl //initial test position double yTop = (oneEventPerRow.get()) - ? (localMax + MINIMUM_EVENT_NODE_GAP) - : computeYTop(minY, h, maxXatY, xLeft, xRight); // if onePerRow, just put it at end + ? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end + : computeYTop(minY, h, maxXatY, xLeft, xRight); localMax = Math.max(yTop + h, localMax); @@ -545,6 +546,23 @@ public final class EventDetailsChart extends XYChart impl return localMax; //return new max } + /** + * Given information about the current layout pass so far and about a + * particular node, compute the y position of that node. + * + * + * @param yMin the smallest (towards the top of the screen) y position to + * consider + * @param h the height of the node we are trying to position + * @param maxXatY a map from y ranges to the max x within that range. NOTE: + * This map will be updated to include the node in question. + * @param xLeft the left x-cord of the node to position + * @param xRight the left x-cord of the node to position + * + * @return the y position for the node in question. + * + * + */ private double computeYTop(double yMin, double h, TreeRangeMap maxXatY, double xLeft, double xRight) { double yTop = yMin; double yBottom = yTop + h; @@ -589,10 +607,10 @@ public final class EventDetailsChart extends XYChart impl } /** - * expose as public + * expose as protected */ @Override - public void requestChartLayout() { + protected void requestChartLayout() { super.requestChartLayout(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java index e599ce017d..391c498ef4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java @@ -1,7 +1,20 @@ /* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. + * Autopsy Forensic Browser + * + * Copyright 2015 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.sleuthkit.autopsy.timeline.ui.detailview; @@ -14,10 +27,6 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; * More specifically it takes an EventStripeNode and produces a stream of * EventStripes containing the stripes for the given node and all child * eventStripes, ignoring intervening EventCluster nodes. - * - * @see - * #loadSubBundles(org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD.RelativeDetail) - * for usage */ class StripeFlattener implements Function> {