org.sleuthkit.autopsy.imagegallery.gui.navpanel.GroupCellFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.sleuthkit.autopsy.imagegallery.gui.navpanel.GroupCellFactory.java

Source

/*
 * 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);
            });
        }
    }
}