diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java index 6689a87ed4..3f32941725 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java @@ -22,8 +22,9 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; * Level of description shown in UI NOTE: this is a separate concept form * {@link DescriptionLOD} */ -enum DescriptionVisibility { - - HIDDEN, COUNT_ONLY, SHOWN; +public enum DescriptionVisibility { + HIDDEN, + COUNT_ONLY, + SHOWN; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java index 4378eed8b3..e8a5bb8dcb 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java @@ -7,22 +7,21 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.List; import java.util.Set; -import javafx.scene.layout.Pane; import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; /** * */ -public interface DetailViewNode { +public interface DetailViewNode> { - long getStartMillis(); + public long getStartMillis(); - long getEndMillis(); + public long getEndMillis(); public void setDescriptionVisibility(DescriptionVisibility get); - public Pane getSubNodePane(); + public List getSubNodes(); public void setSpanWidths(List spanWidths); @@ -32,10 +31,16 @@ public interface DetailViewNode { public Set getEventIDs(); - public void applySelectionEffect(boolean applied); - public String getDescription(); public EventBundle getBundleDescriptor(); + /** + * apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ + void applyHighlightEffect(boolean applied); + + public void applySelectionEffect(boolean applied); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 773ba836f7..9118e6872a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -30,7 +30,6 @@ import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.concurrent.Task; -import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Orientation; import javafx.scene.Cursor; @@ -101,7 +100,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; * TODO: refactor common code out of this class and CountsChartPane into * {@link AbstractVisualization} */ -public class DetailViewPane extends AbstractVisualization { +public class DetailViewPane extends AbstractVisualization, EventDetailChart> { private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); @@ -121,7 +120,7 @@ public class DetailViewPane extends AbstractVisualization aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - private final ObservableList highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); public ObservableList getAggregatedEvents() { return aggregatedEvents; @@ -150,7 +149,7 @@ public class DetailViewPane extends AbstractVisualization change) -> { + highlightedNodes.addListener((ListChangeListener.Change> change) -> { while (change.next()) { change.getAddedSubList().forEach(aeNode -> { aeNode.applyHighlightEffect(true); @@ -167,7 +166,7 @@ public class DetailViewPane extends AbstractVisualization) (ScrollEvent t) -> { + this.onScrollProperty().set((ScrollEvent t) -> { vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() - t.getDeltaY() / 200.0))); }); @@ -213,8 +212,8 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); selectedNodes.stream().forEach((tn) -> { - for (EventClusterNode n : chart.getNodes((EventClusterNode t) - -> t.getEvent().getDescription().equals(tn.getDescription()))) { + for (DetailViewNode n : chart.getNodes((DetailViewNode t) + -> t.getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); } }); @@ -237,8 +236,8 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); for (TreeItem tn : treeSelectionModel.getSelectedItems()) { - for (EventClusterNode n : chart.getNodes((EventClusterNode t) - -> t.getEvent().getDescription().equals(tn.getValue().getDescription()))) { + for (DetailViewNode n : chart.getNodes((DetailViewNode t) + -> t.getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); } } @@ -358,7 +357,7 @@ public class DetailViewPane extends AbstractVisualization c1, Boolean applied) { c1.applySelectionEffect(applied); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index e5e38d8375..133ab3e511 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -80,7 +80,7 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Represents an {@link EventCluster} in a {@link EventDetailChart}. */ -public class EventClusterNode extends StackPane implements DetailViewNode { +public class EventClusterNode extends StackPane implements DetailViewNode { private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); @@ -148,18 +148,23 @@ public class EventClusterNode extends StackPane implements DetailViewNode { private final Button plusButton = new Button(null, new ImageView(PLUS)) { { - setMinSize(16, 16); - setMaxSize(16, 16); - setPrefSize(16, 16); + configureLODButton(this); } }; private final Button minusButton = new Button(null, new ImageView(MINUS)) { { - setMinSize(16, 16); - setMaxSize(16, 16); - setPrefSize(16, 16); + configureLODButton(this); } }; + + private static void configureLODButton(Button b) { + b.setMinSize(16, 16); + b.setMaxSize(16, 16); + b.setPrefSize(16, 16); + b.setVisible(false); + b.setManaged(false); + } + private final EventDetailChart chart; private SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); @@ -196,10 +201,6 @@ public class EventClusterNode extends StackPane implements DetailViewNode { hBox.setPadding(new Insets(2, 5, 2, 5)); hBox.setAlignment(Pos.CENTER_LEFT); - minusButton.setVisible(false); - plusButton.setVisible(false); - minusButton.setManaged(false); - plusButton.setManaged(false); final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null); BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT); borderPane.setPrefWidth(USE_COMPUTED_SIZE); @@ -322,8 +323,10 @@ public class EventClusterNode extends StackPane implements DetailViewNode { } @Override - public Pane getSubNodePane() { - return subNodePane; + public List getSubNodes() { + return subNodePane.getChildrenUnmodifiable().stream() + .map(EventClusterNode.class::cast) + .collect(Collectors.toList()); } synchronized public EventCluster getEvent() { @@ -413,7 +416,7 @@ public class EventClusterNode extends StackPane implements DetailViewNode { * * @param applied true to apply the highlight 'effect', false to remove it */ - synchronized void applyHighlightEffect(boolean applied) { + public synchronized void applyHighlightEffect(boolean applied) { if (applied) { descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS @@ -457,7 +460,7 @@ public class EventClusterNode extends StackPane implements DetailViewNode { * @param newDescriptionLOD */ synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) { - getSubNodePane().getChildren().clear(); + subNodePane.getChildren().clear(); if (newDescriptionLOD == aggEvent.getDescriptionLOD()) { chart.setRequiresLayout(true); chart.requestChartLayout(); @@ -494,7 +497,7 @@ public class EventClusterNode extends StackPane implements DetailViewNode { try { chart.setCursor(Cursor.WAIT); //assign subNodes and request chart layout - getSubNodePane().getChildren().setAll(get()); + subNodePane.getChildren().setAll(get()); setDescriptionVisibility(descrVis); chart.setRequiresLayout(true); chart.requestChartLayout(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java index b5266f8207..8dadf6937a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; @@ -167,7 +168,7 @@ public final class EventDetailChart extends XYChart impl */ private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false); - private final ObservableMap projectionMap = FXCollections.observableHashMap(); + private final ObservableMap, Line> projectionMap = FXCollections.observableHashMap(); /** * flag indicating whether this chart actually needs a layout pass @@ -175,7 +176,7 @@ public final class EventDetailChart extends XYChart impl @GuardedBy(value = "this") private boolean requiresLayout = true; - final ObservableList selectedNodes; + final ObservableList> selectedNodes; /** * list of series of data added to this chart TODO: replace this with a map @@ -205,7 +206,7 @@ public final class EventDetailChart extends XYChart impl private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true); - EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList selectedNodes) { + EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { super(dateAxis, verticalAxis); dateAxis.setAutoRanging(false); @@ -285,7 +286,7 @@ public final class EventDetailChart extends XYChart impl setOnMouseReleased(dragHandler); setOnMouseDragged(dragHandler); - projectionMap.addListener((MapChangeListener.Change change) -> { + projectionMap.addListener((MapChangeListener.Change, ? extends Line> change) -> { final Line valueRemoved = change.getValueRemoved(); if (valueRemoved != null) { getChartChildren().removeAll(valueRemoved); @@ -298,12 +299,12 @@ public final class EventDetailChart extends XYChart impl this.selectedNodes = selectedNodes; this.selectedNodes.addListener(( - ListChangeListener.Change c) -> { + ListChangeListener.Change> c) -> { while (c.next()) { - c.getRemoved().forEach((DetailViewNode t) -> { + c.getRemoved().forEach((DetailViewNode t) -> { projectionMap.remove(t); }); - c.getAddedSubList().forEach((DetailViewNode t) -> { + c.getAddedSubList().forEach((DetailViewNode t) -> { Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET ); @@ -316,7 +317,7 @@ public final class EventDetailChart extends XYChart impl } this.controller.selectEventIDs(selectedNodes.stream() - .flatMap((DetailViewNode aggNode) -> aggNode.getEventIDs().stream()) + .flatMap(detailNode -> detailNode.getEventIDs().stream()) .collect(Collectors.toList())); }); @@ -420,7 +421,7 @@ public final class EventDetailChart extends XYChart impl return EventStripe.merge(u, v); } ); - EventStripeNode clusterNode = new EventStripeNode(eventCluster,null, EventDetailChart.this); + EventStripeNode clusterNode = new EventStripeNode(eventCluster, null, EventDetailChart.this); stripeNodeMap.put(eventCluster, clusterNode); nodeGroup.getChildren().add(clusterNode); } else { @@ -486,11 +487,11 @@ public final class EventDetailChart extends XYChart impl if (bandByType.get() == false) { if (alternateLayout.get() == true) { List nodes = new ArrayList<>(stripeNodeMap.values()); - Collections.sort(nodes, Comparator.comparing(DetailViewNode::getStartMillis)); + nodes.sort(Comparator.comparing(DetailViewNode::getStartMillis)); layoutNodes(nodes, minY, 0); } else { List nodes = new ArrayList<>(nodeMap.values()); - Collections.sort(nodes, Comparator.comparing(DetailViewNode::getStartMillis)); + nodes.sort(Comparator.comparing(DetailViewNode::getStartMillis)); layoutNodes(nodes, minY, 0); } @@ -545,22 +546,28 @@ public final class EventDetailChart extends XYChart impl return descrVisibility; } - synchronized ReadOnlyDoubleProperty - getMaxVScroll() { + synchronized ReadOnlyDoubleProperty getMaxVScroll() { return maxY.getReadOnlyProperty(); } - Iterable getNodes(Predicate p) { - List nodes = new ArrayList<>(); + Iterable> getNodes(Predicate> p) { + Collection> values = alternateLayout.get() + ? stripeNodeMap.values() + : nodeMap.values(); - for (EventClusterNode node : nodeMap.values()) { - checkNode(node, p, nodes); - } - - return nodes; + //collapse tree of DetailViewNoeds to list and then filter on given predicate + return values.stream() + .flatMap(EventDetailChart::flatten) + .filter(p).collect(Collectors.toList()); } - Iterable getAllNodes() { + public static Stream> flatten(DetailViewNode node) { + return Stream.concat( + Stream.of(node), + node.getSubNodes().stream().flatMap(EventDetailChart::flatten)); + } + + Iterable> getAllNodes() { return getNodes(x -> true); } @@ -576,17 +583,6 @@ public final class EventDetailChart extends XYChart impl nodeGroup.setTranslateY(-d * h); } - private static void checkNode(EventClusterNode node, Predicate p, List nodes) { - if (node != null) { - if (p.test(node)) { - nodes.add(node); - } - for (Node n : node.getSubNodePane().getChildrenUnmodifiable()) { - checkNode((EventClusterNode) n, p, nodes); - } - } - } - private void clearGuideLine() { getChartChildren().remove(guideLine); guideLine = null; @@ -599,12 +595,12 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - private synchronized double layoutNodes(final Collection nodes, final double minY, final double xOffset) { + private synchronized > double layoutNodes(final Collection nodes, final double minY, final double xOffset) { //hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot Map maxXatY = new HashMap<>(); double localMax = minY; //for each node lay size it and position it in first available slot - for (D n : nodes) { + for (DVRegion n : nodes) { n.setDescriptionVisibility(descrVisibility.get()); double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(n.getStartMillis())); @@ -613,27 +609,19 @@ public final class EventDetailChart extends XYChart impl double layoutNodesResultHeight = 0; double span = 0; + List subNodes = n.getSubNodes(); + if (subNodes.isEmpty() == false) { + subNodes.sort(Comparator.comparing((DVRegion t) -> t.getStartMillis())); + layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition); + } + if (n instanceof EventClusterNode) { - if (n.getSubNodePane().getChildren().isEmpty() == false) { - List children = n.getSubNodePane().getChildren().stream() - .map(EventClusterNode.class::cast) - .sorted(Comparator.comparing(DetailViewNode::getStartMillis)) - .collect(Collectors.toList()); - layoutNodesResultHeight = layoutNodes(children, 0, rawDisplayPosition); - } double endX = getXAxis().getDisplayPosition(new DateTime(n.getEndMillis())) - xOffset; span = endX - startX; - //size timespan border n.setSpanWidths(Arrays.asList(span)); } else { - if (n.getSubNodePane().getChildren().isEmpty() == false) { - List children = n.getSubNodePane().getChildren().stream() - .map(EventStripeNode.class::cast) - .sorted(Comparator.comparing(DetailViewNode::getStartMillis)) - .collect(Collectors.toList()); - layoutNodesResultHeight = layoutNodes(children, 0, rawDisplayPosition); - } + EventStripeNode cn = (EventStripeNode) n; List spanWidths = new ArrayList<>(); double x = getXAxis().getDisplayPosition(new DateTime(cn.getStartMillis()));; @@ -720,8 +708,8 @@ public final class EventDetailChart extends XYChart impl private static final int DEFAULT_ROW_HEIGHT = 24; private void layoutProjectionMap() { - for (final Map.Entry entry : projectionMap.entrySet()) { - final DetailViewNode aggNode = entry.getKey(); + for (final Map.Entry, Line> entry : projectionMap.entrySet()) { + final DetailViewNode aggNode = entry.getKey(); final Line line = entry.getValue(); line.setStartX(getParentXForValue(new DateTime(aggNode.getStartMillis(), TimeLineController.getJodaTimeZone()))); @@ -760,7 +748,7 @@ public final class EventDetailChart extends XYChart impl return alternateLayout; } - private static class StartTimeComparator implements Comparator { + private static class StartTimeComparator> implements Comparator { @Override public int compare(T n1, T n2) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index aab34c242a..3953b2baba 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -18,6 +18,7 @@ import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; @@ -29,7 +30,6 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.Border; -import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderStroke; import javafx.scene.layout.BorderStrokeStyle; import javafx.scene.layout.BorderWidths; @@ -41,6 +41,7 @@ import javafx.scene.layout.Region; import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; import static javafx.scene.layout.Region.USE_PREF_SIZE; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; @@ -64,7 +65,7 @@ import org.sleuthkit.datamodel.SleuthkitCase; /** * */ -public class EventStripeNode extends StackPane implements DetailViewNode { +public class EventStripeNode extends StackPane implements DetailViewNode { private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); @@ -94,20 +95,28 @@ public class EventStripeNode extends StackPane implements DetailViewNode { private final ImageView tagIV = new ImageView(TAG); private final Button plusButton = new Button(null, new ImageView(PLUS)) { { - setMinSize(16, 16); - setMaxSize(16, 16); - setPrefSize(16, 16); + configureLODButton(this); } }; private final Button minusButton = new Button(null, new ImageView(MINUS)) { { - setMinSize(16, 16); - setMaxSize(16, 16); - setPrefSize(16, 16); + configureLODButton(this); } }; + + private static void configureLODButton(Button b) { + b.setMinSize(16, 16); + b.setMaxSize(16, 16); + b.setPrefSize(16, 16); + show(b, false); + } + + private static void show(Node b, boolean show) { + b.setVisible(show); + b.setManaged(show); + } private DescriptionVisibility descrVis; - private final HBox spanRegion = new HBox(); + private final HBox spansHBox = new HBox(); /** * The IamgeView used to show the icon for this node's event's type */ @@ -127,43 +136,36 @@ public class EventStripeNode extends StackPane implements DetailViewNode { final Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); - final HBox hBox = new HBox(descrLabel, countLabel, spacer, hashIV, tagIV, minusButton, plusButton); + final HBox header = new HBox(descrLabel, countLabel, hashIV, tagIV, spacer, minusButton, plusButton); if (cluster.getEventIDsWithHashHits().isEmpty()) { - hashIV.setManaged(false); - hashIV.setVisible(false); + show(hashIV, false); } if (cluster.getEventIDsWithTags().isEmpty()) { - tagIV.setManaged(false); - tagIV.setVisible(false); + show(tagIV, false); } - hBox.setPrefWidth(USE_COMPUTED_SIZE); - hBox.setMinWidth(USE_PREF_SIZE); - hBox.setPadding(new Insets(2, 5, 2, 5)); - hBox.setAlignment(Pos.CENTER_LEFT); + header.setMinWidth(USE_PREF_SIZE); + header.setPadding(new Insets(2, 5, 2, 5)); + header.setAlignment(Pos.CENTER_LEFT); - minusButton.setVisible(false); - plusButton.setVisible(false); - minusButton.setManaged(false); - plusButton.setManaged(false); - final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null); - BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT); - borderPane.setPrefWidth(USE_COMPUTED_SIZE); + final VBox internalVBox = new VBox(header, subNodePane); + internalVBox.setAlignment(Pos.CENTER_LEFT); final Color evtColor = cluster.getType().getColor(); spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)); for (Range r : cluster.getRanges()) { - Region region = new Region(); - region.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS - region.setBackground(spanFill); - spanRegion.getChildren().addAll(region, new Region()); + Region spanRegion = new Region(); + spanRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS + spanRegion.setBackground(spanFill); + spansHBox.getChildren().addAll(spanRegion, new Region()); } - spanRegion.getChildren().remove(spanRegion.getChildren().size() - 1); - - getChildren().addAll(spanRegion, borderPane); - setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + spansHBox.getChildren().remove(spansHBox.getChildren().size() - 1); + spansHBox.setMaxWidth(USE_PREF_SIZE); + setMaxWidth(USE_PREF_SIZE); + getChildren().addAll(spansHBox, internalVBox); + setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .05), CORNER_RADII, Insets.EMPTY))); setAlignment(Pos.TOP_LEFT); setMinHeight(24); - minWidthProperty().bind(spanRegion.widthProperty()); + minWidthProperty().bind(spansHBox.widthProperty()); setPrefHeight(USE_COMPUTED_SIZE); setMaxHeight(USE_PREF_SIZE); @@ -189,20 +191,20 @@ public class EventStripeNode extends StackPane implements DetailViewNode { setOnMouseEntered((MouseEvent e) -> { //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart // installTooltip(); - spanRegion.setEffect(new DropShadow(10, evtColor)); - minusButton.setVisible(true); - plusButton.setVisible(true); - minusButton.setManaged(true); - plusButton.setManaged(true); + spansHBox.setEffect(new DropShadow(10, evtColor)); + show(spacer, true); + show(minusButton, true); + show(plusButton, true); + toFront(); }); setOnMouseExited((MouseEvent e) -> { - spanRegion.setEffect(null); - minusButton.setVisible(false); - plusButton.setVisible(false); - minusButton.setManaged(false); - plusButton.setManaged(false); + spansHBox.setEffect(null); + show(spacer, false); + show(minusButton, false); + show(plusButton, false); + }); plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); @@ -232,11 +234,11 @@ public class EventStripeNode extends StackPane implements DetailViewNode { @Override public void setSpanWidths(List spanWidths) { for (int i = 0; i < spanWidths.size(); i++) { - Region get = (Region) spanRegion.getChildren().get(i); + Region spanRegion = (Region) spansHBox.getChildren().get(i); Double w = spanWidths.get(i); - get.setPrefWidth(w); - get.setMaxWidth(w); - get.setMinWidth(Math.max(2, w)); + spanRegion.setPrefWidth(w); + spanRegion.setMaxWidth(w); + spanRegion.setMinWidth(Math.max(2, w)); } } @@ -280,8 +282,10 @@ public class EventStripeNode extends StackPane implements DetailViewNode { } @Override - public Pane getSubNodePane() { - return subNodePane; + public List getSubNodes() { + return subNodePane.getChildrenUnmodifiable().stream() + .map(EventStripeNode.class::cast) + .collect(Collectors.toList()); } /** @@ -339,6 +343,26 @@ public class EventStripeNode extends StackPane implements DetailViewNode { }); } + /** + * apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ + @Override + public synchronized void applyHighlightEffect(boolean applied) { + if (applied) { + descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS + spanFill = new Background(new BackgroundFill(cluster.getType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)); + spansHBox.setBackground(spanFill); + setBackground(new Background(new BackgroundFill(cluster.getType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); + } else { + descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS + spanFill = new Background(new BackgroundFill(cluster.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); + spansHBox.setBackground(spanFill); + setBackground(new Background(new BackgroundFill(cluster.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + } + } + @Override public String getDescription() { return cluster.getDescription(); @@ -355,11 +379,14 @@ public class EventStripeNode extends StackPane implements DetailViewNode { * @param newDescriptionLOD */ synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) { - getSubNodePane().getChildren().clear(); + subNodePane.getChildren().clear(); + if (newDescriptionLOD == cluster.getDescriptionLOD()) { + spansHBox.setVisible(true); chart.setRequiresLayout(true); chart.requestChartLayout(); } else { + spansHBox.setVisible(false); RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf(); //make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters combinedFilter.getSubFilters().addAll(new TextFilter(cluster.getDescription()), @@ -402,7 +429,7 @@ public class EventStripeNode extends StackPane implements DetailViewNode { try { chart.setCursor(Cursor.WAIT); //assign subNodes and request chart layout - getSubNodePane().getChildren().setAll(get()); + subNodePane.getChildren().setAll(get()); setDescriptionVisibility(descrVis); chart.setRequiresLayout(true); chart.requestChartLayout();