org.sleuthkit.autopsy.timeline.ui.detailview.AggregateEventNode.java Source code

Java tutorial

Introduction

Here is the source code for org.sleuthkit.autopsy.timeline.ui.detailview.AggregateEventNode.java

Source

/*
 * Autopsy Forensic Browser
 *
 * Copyright 2013 Basis Technology Corp.
 * Contact: carrier <at> sleuthkit <dot> 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;

import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
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;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
import org.sleuthkit.autopsy.coreutils.LoggedTask;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
import org.sleuthkit.autopsy.timeline.filters.Filter;
import org.sleuthkit.autopsy.timeline.filters.TextFilter;
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;

/** Represents an {@link AggregateEvent} in a {@link EventDetailChart}. */
public class AggregateEventNode extends StackPane {

    private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
    private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS

    private static final CornerRadii CORNER_RADII = new CornerRadii(3);

    /** the border to apply when this node is 'selected' */
    private static final Border selectionBorder = new Border(
            new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));

    /** The event this AggregateEventNode represents visually */
    private final AggregateEvent event;

    private final AggregateEventNode parentEventNode;

    /** the region that represents the time span of this node's event */
    private final Region spanRegion = new Region();

    /** The label used to display this node's event's description */
    private final Label descrLabel = new Label();

    /** The label used to display this node's event count */
    private final Label countLabel = new Label();

    /** The IamgeView used to show the icon for this node's event's type */
    private final ImageView eventTypeImageView = new ImageView();

    /** Pane that contains AggregateEventNodes of any 'subevents' if they are
     * displayed
     *
     * //TODO: move more of the control of subnodes/events here and out of
     * EventDetail Chart */
    private final Pane subNodePane = new Pane();

    /** the context menu that with the slider that controls subnode/event
     * display
     *
     * //TODO: move more of the control of subnodes/events here and out
     * of EventDetail Chart */
    private final SimpleObjectProperty<ContextMenu> contextMenu = new SimpleObjectProperty<>();

    /** the Background used to fill the spanRegion, this varies epending on the
     * selected/highlighted state of this node in its parent EventDetailChart */
    private Background spanFill;

    private final Button plusButton = new Button(null, new ImageView(PLUS)) {
        {
            setMinSize(16, 16);
            setMaxSize(16, 16);
            setPrefSize(16, 16);
        }
    };
    private final Button minusButton = new Button(null, new ImageView(MINUS)) {
        {
            setMinSize(16, 16);
            setMaxSize(16, 16);
            setPrefSize(16, 16);
        }
    };
    private final EventDetailChart chart;

    private SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
    private DescriptionVisibility descrVis;

    public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentEventNode,
            EventDetailChart chart) {
        this.event = event;
        descLOD.set(event.getLOD());
        this.parentEventNode = parentEventNode;
        this.chart = chart;
        final Region region = new Region();
        HBox.setHgrow(region, Priority.ALWAYS);
        final HBox hBox = new HBox(descrLabel, countLabel, region, minusButton, plusButton);
        hBox.setPrefWidth(USE_COMPUTED_SIZE);
        hBox.setMinWidth(USE_PREF_SIZE);
        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);

        getChildren().addAll(spanRegion, borderPane);

        setAlignment(Pos.TOP_LEFT);
        setMinHeight(24);
        minWidthProperty().bind(spanRegion.widthProperty());
        setPrefHeight(USE_COMPUTED_SIZE);
        setMaxHeight(USE_PREF_SIZE);

        //set up subnode pane sizing contraints
        subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
        subNodePane.setMinHeight(USE_PREF_SIZE);
        subNodePane.setMinWidth(USE_PREF_SIZE);
        subNodePane.setMaxHeight(USE_PREF_SIZE);
        subNodePane.setMaxWidth(USE_PREF_SIZE);
        subNodePane.setPickOnBounds(false);

        //setup description label
        eventTypeImageView.setImage(event.getType().getFXImage());
        descrLabel.setGraphic(eventTypeImageView);
        descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
        descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);

        descrLabel.setMouseTransparent(true);
        setDescriptionVisibility(chart.getDescrVisibility().get());

        //setup backgrounds
        final Color evtColor = event.getType().getColor();
        spanFill = new Background(
                new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
        setBackground(
                new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
        setCursor(Cursor.HAND);
        spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: "
                + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS
        spanRegion.setBackground(spanFill);

        //set up mouse hover effect and tooltip
        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);
            toFront();

        });

        setOnMouseExited((MouseEvent e) -> {
            spanRegion.setEffect(null);
            minusButton.setVisible(false);
            plusButton.setVisible(false);
            minusButton.setManaged(false);
            plusButton.setManaged(false);

        });

        setOnMouseClicked(new EventMouseHandler());

        plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
        minusButton.disableProperty().bind(descLOD.isEqualTo(event.getLOD()));

        plusButton.setOnMouseClicked(e -> {
            final DescriptionLOD next = descLOD.get().next();
            if (next != null) {
                loadSubClusters(next);
                descLOD.set(next);
            }
        });
        minusButton.setOnMouseClicked(e -> {
            final DescriptionLOD previous = descLOD.get().previous();
            if (previous != null) {
                loadSubClusters(previous);
                descLOD.set(previous);
            }
        });
    }

    private void installTooltip() {
        Tooltip.install(AggregateEventNode.this,
                new Tooltip(NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text",
                        getEvent().getEventIDs().size(), getEvent().getType(), getEvent().getDescription(),
                        getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter()),
                        getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()))));
    }

    public Pane getSubNodePane() {
        return subNodePane;
    }

    public AggregateEvent getEvent() {
        return event;
    }

    /**
     * sets the width of the {@link Region} with border and background used to
     * indicate the temporal span of this aggregate event
     *
     * @param w
     */
    public void setSpanWidth(double w) {
        spanRegion.setPrefWidth(w);
        spanRegion.setMaxWidth(w);
        spanRegion.setMinWidth(Math.max(2, w));
    }

    /**
     *
     * @param w the maximum width the description label should have
     */
    public void setDescriptionWidth(double w) {
        descrLabel.setMaxWidth(w);
    }

    /** @param descrVis the level of description that should be displayed */
    final void setDescriptionVisibility(DescriptionVisibility descrVis) {
        this.descrVis = descrVis;
        final int size = event.getEventIDs().size();

        switch (descrVis) {

        case COUNT_ONLY:
            descrLabel.setText("");
            countLabel.setText(String.valueOf(size));
            break;
        case HIDDEN:
            countLabel.setText("");
            descrLabel.setText("");
            break;
        default:
        case SHOWN:
            String description = event.getDescription();
            description = parentEventNode != null
                    ? "    ..."
                            + StringUtils.substringAfter(description, parentEventNode.getEvent().getDescription())
                    : description;
            descrLabel.setText(description);
            countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
            break;
        }
    }

    /** apply the 'effect' to visually indicate selection
     *
     * @param applied true to apply the selection 'effect', false to remove it
     */
    void applySelectionEffect(final boolean applied) {
        Platform.runLater(() -> {
            if (applied) {
                setBorder(selectionBorder);
            } else {
                setBorder(null);
            }
        });
    }

    /** apply the 'effect' to visually indicate highlighted nodes
     *
     * @param applied true to apply the highlight 'effect', false to remove it
     */
    void applyHighlightEffect(boolean applied) {

        if (applied) {
            descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
            spanFill = new Background(new BackgroundFill(getEvent().getType().getColor().deriveColor(0, 1, 1, .3),
                    CORNER_RADII, Insets.EMPTY));
            spanRegion.setBackground(spanFill);
            setBackground(new Background(new BackgroundFill(
                    getEvent().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(getEvent().getType().getColor().deriveColor(0, 1, 1, .1),
                    CORNER_RADII, Insets.EMPTY));
            spanRegion.setBackground(spanFill);
            setBackground(new Background(new BackgroundFill(
                    getEvent().getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
        }
    }

    String getDisplayedDescription() {
        return descrLabel.getText();
    }

    double getLayoutXCompensation() {
        return (parentEventNode != null ? parentEventNode.getLayoutXCompensation() : 0)
                + getBoundsInParent().getMinX();
    }

    /**
     * @return the contextMenu
     */
    public ContextMenu getContextMenu() {
        return contextMenu.get();
    }

    /**
     * @param contextMenu the contextMenu to set
     */
    public void setContextMenu(ContextMenu contextMenu) {
        this.contextMenu.set(contextMenu);
    }

    /**
     * loads sub-clusters at the given Description LOD
     *
     * @param newLOD
     */
    private void loadSubClusters(DescriptionLOD newLOD) {
        getSubNodePane().getChildren().clear();
        if (newLOD == event.getLOD()) {
            getSubNodePane().getChildren().clear();
            chart.setRequiresLayout(true);
            chart.requestChartLayout();
        } else {
            //make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters
            final Filter combinedFilter = Filter.intersect(new Filter[] { new TextFilter(event.getDescription()),
                    new TypeFilter(event.getType()), chart.getFilteredEvents().filter().get() });

            //make a new end inclusive span (to 'filter' with)
            final Interval span = event.getSpan().withEndMillis(event.getSpan().getEndMillis() + 1000);

            //make a task to load the subnodes
            LoggedTask<List<AggregateEventNode>> loggedTask = new LoggedTask<List<AggregateEventNode>>(
                    NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {

                @Override
                protected List<AggregateEventNode> call() throws Exception {
                    //query for the sub-clusters
                    List<AggregateEvent> aggregatedEvents = chart.getFilteredEvents()
                            .getAggregatedEvents(new ZoomParams(span,
                                    chart.getFilteredEvents().eventTypeZoom().get(), combinedFilter, newLOD));
                    //for each sub cluster make an AggregateEventNode to visually represent it, and set x-position
                    return aggregatedEvents.stream().map((AggregateEvent t) -> {
                        AggregateEventNode subNode = new AggregateEventNode(t, AggregateEventNode.this, chart);
                        subNode.setLayoutX(
                                chart.getXAxis().getDisplayPosition(new DateTime(t.getSpan().getStartMillis()))
                                        - getLayoutXCompensation());
                        return subNode;
                    }).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters
                }

                @Override
                protected void succeeded() {
                    try {
                        chart.setCursor(Cursor.WAIT);
                        //assign subNodes and request chart layout
                        getSubNodePane().getChildren().setAll(get());
                        setDescriptionVisibility(descrVis);
                        chart.setRequiresLayout(true);
                        chart.requestChartLayout();
                        chart.setCursor(null);
                    } catch (InterruptedException | ExecutionException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                }
            };

            //start task
            chart.getController().monitorTask(loggedTask);
        }
    }

    /** event handler used for mouse events on {@link AggregateEventNode}s */
    private class EventMouseHandler implements EventHandler<MouseEvent> {

        @Override
        public void handle(MouseEvent t) {
            if (t.getButton() == MouseButton.PRIMARY) {
                t.consume();
                if (t.isShiftDown()) {
                    if (chart.selectedNodes.contains(AggregateEventNode.this) == false) {
                        chart.selectedNodes.add(AggregateEventNode.this);
                    }
                } else if (t.isShortcutDown()) {
                    chart.selectedNodes.removeAll(AggregateEventNode.this);
                } else if (t.getClickCount() > 1) {
                    final DescriptionLOD next = descLOD.get().next();
                    if (next != null) {
                        loadSubClusters(next);
                        descLOD.set(next);
                    }
                } else {
                    chart.selectedNodes.setAll(AggregateEventNode.this);
                }
            }
        }
    }
}