/* * Autopsy Forensic Browser * * Copyright 2016 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.imagegallery.gui.navpanel; import static java.util.Objects.isNull; import java.util.Optional; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.scene.Node; import javafx.scene.control.Cell; import javafx.scene.control.Control; import javafx.scene.control.Labeled; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.OverrunStyle; import javafx.scene.control.Tooltip; import javafx.scene.control.TreeCell; import javafx.scene.control.TreeView; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import org.apache.commons.lang3.StringUtils; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; import org.sleuthkit.datamodel.TagName; /** * A Factory for Cells to use in a ListView<DrawableGroup> or * TreeView<GroupTreeNode> */ class GroupCellFactory { /** * icon to use if a cell doesn't represent a group but just a folder(with no * DrawableFiles) in the file system hierarchy. */ private static final Image EMPTY_FOLDER_ICON = new Image("/org/sleuthkit/autopsy/imagegallery/images/folder.png"); //NON-NLS private final ReadOnlyObjectProperty<GroupComparators<?>> sortOrder; private final ImageGalleryController controller; GroupCellFactory(ImageGalleryController controller, ReadOnlyObjectProperty<GroupComparators<?>> sortOrderProperty) { this.controller = controller; this.sortOrder = sortOrderProperty; } GroupListCell getListCell(ListView<DrawableGroup> listview) { return initCell(new GroupListCell()); } GroupTreeCell getTreeCell(TreeView<?> treeView) { return initCell(new GroupTreeCell()); } /** * remove the listener when it is not needed any more * * @param listener * @param oldGroup */ private void removeListeners(InvalidationListener listener, DrawableGroup oldGroup) { sortOrder.removeListener(listener); oldGroup.getFileIDs().removeListener(listener); oldGroup.seenProperty().removeListener(listener); oldGroup.uncatCountProperty().removeListener(listener); oldGroup.hashSetHitsCountProperty().removeListener(listener); } private void addListeners(InvalidationListener listener, DrawableGroup group) { //if the sort order changes, update the counts displayed to match the sorted by property sortOrder.addListener(listener); //if number of files in this group changes (eg a file is recategorized), update counts via listener group.getFileIDs().addListener(listener); group.uncatCountProperty().addListener(listener); group.hashSetHitsCountProperty().addListener(listener); //if the seen state of this group changes update its style group.seenProperty().addListener(listener); } private <X extends Cell<?> & GroupCell<?>> X initCell(X cell) { /* * reduce indent of TreeCells to 5, default is 10 which uses up a lot of * space. Define seen and unseen styles */ cell.getStylesheets().add(GroupCellFactory.class.getResource("GroupCell.css").toExternalForm()); //NON-NLS cell.getStyleClass().add("groupCell"); //NON-NLS //since end of path is probably more interesting put ellipsis at front cell.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS); Platform.runLater(() -> cell.prefWidthProperty().bind(cell.getView().widthProperty().subtract(15))); return cell; } private <X extends Cell<?> & GroupCell<?>> void updateGroup(X cell, DrawableGroup group) { addListeners(cell.getGroupListener(), group); //and use icon corresponding to group type final Node graphic = (group.getGroupByAttribute() == DrawableAttribute.TAGS) ? controller.getTagsManager().getGraphic((TagName) group.getGroupByValue()) : group.getGroupKey().getGraphic(); final String text = getCellText(cell); final String style = getSeenStyleClass(cell); Platform.runLater(() -> { cell.setTooltip(new Tooltip(text)); cell.setGraphic(graphic); cell.setText(text); cell.setStyle(style); }); } private <X extends Labeled & GroupCell<?>> void clearCell(X cell) { Platform.runLater(() -> { cell.setTooltip(null); cell.setText(null); cell.setGraphic(null); cell.setStyle(""); }); } /** * return the styleClass to apply based on the assigned group's seen status * * @return the style class to apply */ private String getSeenStyleClass(GroupCell<?> cell) { return cell.getGroup() .map(DrawableGroup::isSeen) .map(seen -> seen ? "" : "-fx-font-weight:bold;") //NON-NLS .orElse(""); //if item is null or group is null } /** * get the counts part of the text to apply to this cell, including * parentheses * * @return get the counts part of the text to apply to this cell */ private String getCountsText(GroupCell<?> cell) { return cell.getGroup() .map(group -> " (" + (sortOrder.get() == GroupComparators.ALPHABETICAL ? group.getSize() : sortOrder.get().getFormattedValueOfGroup(group)) + ")" ).orElse(""); //if item is null or group is null } private String getCellText(GroupCell<?> cell) { return cell.getGroupName() + getCountsText(cell); } private class GroupTreeCell extends TreeCell<GroupTreeNode> implements GroupCell<TreeView<GroupTreeNode>> { private final InvalidationListener groupListener = new GroupListener<>(this); /** * reference to group files listener that allows us to remove it from a * group when a new group is assigned to this Cell */ @Override public InvalidationListener getGroupListener() { return groupListener; } @Override public TreeView<GroupTreeNode> getView() { return getTreeView(); } @Override public String getGroupName() { return Optional.ofNullable(getItem()) .map(treeNode -> StringUtils.defaultIfBlank(treeNode.getPath(), DrawableGroup.getBlankGroupName())) .orElse(""); } @Override public Optional<DrawableGroup> getGroup() { return Optional.ofNullable(getItem()) .map(GroupTreeNode::getGroup); } @Override protected synchronized void updateItem(final GroupTreeNode newItem, boolean empty) { //if there was a previous group, remove the listeners getGroup().ifPresent(oldGroup -> removeListeners(getGroupListener(), oldGroup)); super.updateItem(newItem, empty); if (isNull(newItem) || empty) { clearCell(this); } else { DrawableGroup newGroup = newItem.getGroup(); if (isNull(newGroup)) { //this cod epath should only be invoked for non-group Tree final String groupName = getGroupName(); //"dummy" group in file system tree <=> a folder with no drawables Platform.runLater(() -> { setTooltip(new Tooltip(groupName)); setText(groupName); setGraphic(new ImageView(EMPTY_FOLDER_ICON)); setStyle(""); }); } else { updateGroup(this, newGroup); } } } } private class GroupListCell extends ListCell<DrawableGroup> implements GroupCell<ListView<DrawableGroup>> { private final InvalidationListener groupListener = new GroupListener<>(this); /** * reference to group files listener that allows us to remove it from a * group when a new group is assigned to this Cell */ @Override public InvalidationListener getGroupListener() { return groupListener; } @Override public ListView<DrawableGroup> getView() { return getListView(); } @Override public String getGroupName() { return Optional.ofNullable(getItem()) .map(group -> group.getGroupByValueDislpayName()) .orElse(""); } @Override public Optional<DrawableGroup> getGroup() { return Optional.ofNullable(getItem()); } @Override protected synchronized void updateItem(final DrawableGroup newGroup, boolean empty) { //if there was a previous group, remove the listeners getGroup().ifPresent(oldGroup -> removeListeners(getGroupListener(), oldGroup)); super.updateItem(newGroup, empty); if (isNull(newGroup) || empty) { clearCell(this); } else { updateGroup(this, newGroup); } } } private interface GroupCell<X extends Control> { String getGroupName(); X getView(); Optional<DrawableGroup> getGroup(); InvalidationListener getGroupListener(); } private class GroupListener<X extends Labeled & GroupCell<?>> implements InvalidationListener { private final X cell; GroupListener(X cell) { this.cell = cell; } @Override public void invalidated(Observable o) { final String text = getCellText(cell); final String style = getSeenStyleClass(cell); Platform.runLater(() -> { cell.setText(text); cell.setTooltip(new Tooltip(text)); cell.setStyle(style); }); } } }