/* * Autopsy Forensic Browser * * Copyright 2013-15 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.tree; import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.Collection; import java.util.Objects; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.SelectionMode; import javafx.scene.control.Tooltip; import javafx.scene.control.TreeCell; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionUtils; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; /** * Shows all EventBundles from the assigned DetailViewPane in a * tree organized by type and then description. Hidden bundles are shown grayed * out. Right clicking on a item in the tree shows a context menu to show/hide * it. */ final public class EventsTree extends BorderPane { private final TimeLineController controller; private DetailViewPane detailViewPane; @FXML private TreeView<TimeLineEvent> eventsTree; @FXML private Label eventsTreeLabel; @FXML private ComboBox<TreeComparator> sortByBox; private final ObservableList<TimeLineEvent> selectedEvents = FXCollections.observableArrayList(); public EventsTree(TimeLineController controller) { this.controller = controller; FXMLConstructor.construct(this, "EventsTree.fxml"); // NON-NLS } public void setDetailViewPane(DetailViewPane detailViewPane) { this.detailViewPane = detailViewPane; detailViewPane.getAllNestedEvents().addListener((ListChangeListener.Change<? extends TimeLineEvent> c) -> { //on jfx thread while (c.next()) { c.getRemoved().forEach(getRoot()::remove); c.getAddedSubList().forEach(getRoot()::insert); } }); setRoot(); detailViewPane.getSelectedEvents().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); detailViewPane.getSelectedEvents().forEach(event -> { eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(event)); }); }); } private RootItem getRoot() { return (RootItem) eventsTree.getRoot(); } @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void setRoot() { RootItem root = new RootItem(TreeComparator.Type.reversed().thenComparing(sortByBox.getSelectionModel().getSelectedItem())); detailViewPane.getAllNestedEvents().forEach(root::insert); eventsTree.setRoot(root); } @FXML @NbBundle.Messages("EventsTree.Label.text=Sort By:") void initialize() { assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'NavPanel.fxml'."; // NON-NLS sortByBox.getItems().setAll(Arrays.asList(TreeComparator.Description, TreeComparator.Count)); sortByBox.getSelectionModel().select(TreeComparator.Description); sortByBox.setCellFactory(listView -> new TreeComparatorCell()); sortByBox.setButtonCell(new TreeComparatorCell()); sortByBox.getSelectionModel().selectedItemProperty().addListener(selectedItemProperty -> { getRoot().sort(TreeComparator.Type.reversed().thenComparing(sortByBox.getSelectionModel().getSelectedItem()), true); }); eventsTree.setShowRoot(false); eventsTree.setCellFactory(treeView -> new EventTreeCell()); eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); eventsTree.getSelectionModel().getSelectedItems().addListener((ListChangeListener.Change<? extends TreeItem<TimeLineEvent>> change) -> { while (change.next()) { change.getRemoved().stream().map(TreeItem<TimeLineEvent>::getValue).forEach(selectedEvents::remove); change.getAddedSubList().stream().map(TreeItem<TimeLineEvent>::getValue).filter(Objects::nonNull).forEach(selectedEvents::add); } }); eventsTreeLabel.setText(Bundle.EventsTree_Label_text()); } public ObservableList<TimeLineEvent> getSelectedEvents() { return selectedEvents; } /** * A tree cell to display TimeLineEvents. Shows the description, and count, * as well a a "legend icon" for the event type. */ private class EventTreeCell extends TreeCell<TimeLineEvent> { private static final double HIDDEN_MULTIPLIER = .6; private final Rectangle rect = new Rectangle(24, 24); private final ImageView imageView = new ImageView(); private InvalidationListener filterStateChangeListener; private final SimpleBooleanProperty hidden = new SimpleBooleanProperty(false); EventTreeCell() { rect.setArcHeight(5); rect.setArcWidth(5); rect.setStrokeWidth(2); } @Override protected void updateItem(TimeLineEvent item, boolean empty) { super.updateItem(item, empty); if (empty) { setText(null); setTooltip(null); setGraphic(null); deRegisterListeners(controller.getQuickHideFilters()); } else { EventsTreeItem treeItem = (EventsTreeItem) getTreeItem(); String text = treeItem.getDisplayText(); setText(text); setTooltip(new Tooltip(text)); imageView.setImage(treeItem.getEventType().getFXImage()); setGraphic(new StackPane(rect, imageView)); updateHiddenState(treeItem); deRegisterListeners(controller.getQuickHideFilters()); if (item != null) { filterStateChangeListener = (filterState) -> updateHiddenState(treeItem); controller.getQuickHideFilters().addListener((ListChangeListener.Change<? extends DescriptionFilter> listChange) -> { while (listChange.next()) { deRegisterListeners(listChange.getRemoved()); registerListeners(listChange.getAddedSubList(), item); } updateHiddenState(treeItem); }); registerListeners(controller.getQuickHideFilters(), item); setOnMouseClicked((MouseEvent event) -> { if (event.getButton() == MouseButton.SECONDARY) { Action action = hidden.get() ? detailViewPane.newUnhideDescriptionAction(item.getDescription(), item.getDescriptionLoD()) : detailViewPane.newHideDescriptionAction(item.getDescription(), item.getDescriptionLoD()); ActionUtils.createContextMenu(ImmutableList.of(action)) .show(this, event.getScreenX(), event.getScreenY()); } }); } else { setOnMouseClicked(null); } } } private void registerListeners(Collection<? extends DescriptionFilter> filters, TimeLineEvent item) { for (DescriptionFilter filter : filters) { if (filter.getDescription().equals(item.getDescription())) { filter.activeProperty().addListener(filterStateChangeListener); } } } private void deRegisterListeners(Collection<? extends DescriptionFilter> filters) { if (Objects.nonNull(filterStateChangeListener)) { for (DescriptionFilter filter : filters) { filter.activeProperty().removeListener(filterStateChangeListener); } } } private void updateHiddenState(EventsTreeItem treeItem) { TimeLineEvent event = treeItem.getValue(); hidden.set(event != null && controller.getQuickHideFilters().stream(). filter(DescriptionFilter::isActive) .anyMatch(filter -> StringUtils.equalsIgnoreCase(filter.getDescription(), event.getDescription()))); if (hidden.get()) { treeItem.setExpanded(false); setTextFill(Color.gray(0, HIDDEN_MULTIPLIER)); imageView.setOpacity(HIDDEN_MULTIPLIER); rect.setStroke(treeItem.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, 1, HIDDEN_MULTIPLIER)); rect.setFill(treeItem.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, HIDDEN_MULTIPLIER, 0.1)); } else { setTextFill(Color.BLACK); imageView.setOpacity(1); rect.setStroke(treeItem.getEventType().getColor()); rect.setFill(treeItem.getEventType().getColor().deriveColor(0, 1, 1, 0.1)); } } } static private class TreeComparatorCell extends ListCell<TreeComparator> { @Override protected void updateItem(TreeComparator item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) { setText(null); } else { setText(item.getDisplayName()); } } } }