diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java index 4398f99206..b0b6c92fca 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java @@ -43,7 +43,7 @@ public abstract class AbstractAbstractFileNode extends A private static final Logger LOGGER = Logger.getLogger(AbstractAbstractFileNode.class.getName()); /** - * @param type of the AbstractFile data to encapsulate + * @param type of the AbstractFile data to encapsulate * @param abstractFile file to encapsulate */ AbstractAbstractFileNode(T abstractFile) { @@ -52,7 +52,7 @@ public abstract class AbstractAbstractFileNode extends A int dotIndex = name.lastIndexOf("."); if (dotIndex > 0) { String ext = name.substring(dotIndex).toLowerCase(); - + // If this is an archive file we will listen for ingest events // that will notify us when new content has been identified. for (String s : FileTypeExtensions.getArchiveExtensions()) { @@ -228,6 +228,13 @@ public abstract class AbstractAbstractFileNode extends A public String toString() { return NbBundle.getMessage(this.getClass(), "AbstractAbstractFileNode.objectId"); + } + }, + MIMETYPE { + @Override + public String toString() { + return NbBundle.getMessage(this.getClass(), "AbstractAbstractFileNode.mimeType"); + } }, } @@ -235,8 +242,8 @@ public abstract class AbstractAbstractFileNode extends A /** * Fill map with AbstractFile properties * - * @param map map with preserved ordering, where property names/values - * are put + * @param map map with preserved ordering, where property names/values are + * put * @param content to extract properties from */ public static void fillPropertyMap(Map map, AbstractFile content) { @@ -268,6 +275,7 @@ public abstract class AbstractAbstractFileNode extends A map.put(AbstractFilePropertyType.HASHSETS.toString(), getHashSetHitsForFile(content)); map.put(AbstractFilePropertyType.MD5HASH.toString(), content.getMd5Hash() == null ? "" : content.getMd5Hash()); map.put(AbstractFilePropertyType.ObjectID.toString(), content.getId()); + map.put(AbstractFilePropertyType.MIMETYPE.toString(), content.getMIMEType() == null ? "" : content.getMIMEType()); } static String getContentDisplayName(AbstractFile file) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties index 05d385e858..53724950de 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties @@ -18,6 +18,7 @@ AbstractAbstractFileNode.typeMetaColLbl=Type(Meta) AbstractAbstractFileNode.knownColLbl=Known AbstractAbstractFileNode.inHashsetsColLbl=In Hashsets AbstractAbstractFileNode.md5HashColLbl=MD5 Hash +AbstractAbstractFileNode.mimeType = MIME Type AbstractContentChildren.CreateTSKNodeVisitor.exception.noNodeMsg=No Node defined for the given SleuthkitItem AbstractContentChildren.createAutopsyNodeVisitor.exception.noNodeMsg=No Node defined for the given DisplayableItem AbstractContentNode.exception.cannotChangeSysName.msg=Cannot change the system name. diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FXMLConstructor.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FXMLConstructor.java index e65248f2d7..55d45ac5f5 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FXMLConstructor.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FXMLConstructor.java @@ -23,9 +23,9 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.logging.Level; import javafx.fxml.FXMLLoader; -import javafx.scene.Node; import org.apache.commons.lang3.StringUtils; import org.sleuthkit.autopsy.coreutils.Logger; + /** * This class supports programmer productivity by abstracting frequently used * code to load FXML-defined GUI components, @@ -35,10 +35,10 @@ import org.sleuthkit.autopsy.coreutils.Logger; * http://stackoverflow.com/questions/11734885/javafx2-very-poor-performance-when-adding-custom-made-fxmlpanels-to-gridpane. */ public class FXMLConstructor { - + private static Logger logger = Logger.getLogger(FXMLConstructor.class.getName()); - static public void construct(Node n, String fxmlFileName) { + static public void construct(Object n, String fxmlFileName) { final String name = "nbres:/" + StringUtils.replace(n.getClass().getPackage().getName(), ".", "/") + "/" + fxmlFileName; try { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index 9d374b3478..f532b0e83c 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -75,7 +75,6 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog; import org.sleuthkit.autopsy.imagegallery.gui.Toolbar; -import org.sleuthkit.autopsy.imagegallery.gui.navpanel.NavPanel; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -94,6 +93,7 @@ import org.sleuthkit.datamodel.TskData; public final class ImageGalleryController implements Executor { private final Executor execDelegate = Executors.newSingleThreadExecutor(); + private Runnable showTree; @Override public void execute(Runnable command) { @@ -153,7 +153,7 @@ public final class ImageGalleryController implements Executor { private Node infoOverlay; private SleuthkitCase sleuthKitCase; - private NavPanel navPanel; +// private NavPanel navPanel; public ReadOnlyBooleanProperty getMetaDataCollapsed() { return metaDataCollapsed.getReadOnlyProperty(); @@ -266,8 +266,8 @@ public final class ImageGalleryController implements Executor { @ThreadConfined(type = ThreadConfined.ThreadType.ANY) public void advance(GroupViewState newState, boolean forceShowTree) { - if (Objects.nonNull(navPanel) && forceShowTree) { - navPanel.showTree(); + if (forceShowTree && showTree != null) { + showTree.run(); } historyManager.advance(newState); } @@ -482,8 +482,8 @@ public final class ImageGalleryController implements Executor { return tagsManager; } - public void setNavPanel(NavPanel navPanel) { - this.navPanel = navPanel; + public void setShowTree(Runnable showTree) { + this.showTree = showTree; } public UndoRedoManager getUndoManager() { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java index a2669cf3df..d1e8f3a236 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,9 @@ import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import javafx.scene.Scene; import javafx.scene.control.SplitPane; +import javafx.scene.control.TabPane; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import org.netbeans.api.settings.ConvertAsProperties; @@ -40,7 +42,8 @@ import org.sleuthkit.autopsy.imagegallery.gui.SummaryTablePane; import org.sleuthkit.autopsy.imagegallery.gui.Toolbar; import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.GroupPane; import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.MetaDataPane; -import org.sleuthkit.autopsy.imagegallery.gui.navpanel.NavPanel; +import org.sleuthkit.autopsy.imagegallery.gui.navpanel.GroupTree; +import org.sleuthkit.autopsy.imagegallery.gui.navpanel.HashHitGroupList; /** * Top component which displays ImageGallery interface. @@ -92,7 +95,7 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl } public static void closeTopComponent() { - if(topComponentInitialized){ + if (topComponentInitialized) { final TopComponent etc = WindowManager.getDefault().findTopComponent("ImageGalleryTopComponent"); if (etc != null) { try { @@ -122,7 +125,8 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl private GroupPane groupPane; - private NavPanel navPanel; + private GroupTree groupTree; + private HashHitGroupList hashHitList; private VBox leftPane; @@ -132,7 +136,7 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl setName(Bundle.CTL_ImageGalleryTopComponent()); setToolTipText(Bundle.HINT_ImageGalleryTopComponent()); - + initComponents(); Platform.runLater(() -> {//initialize jfx ui @@ -149,8 +153,13 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl metaDataTable = new MetaDataPane(controller); - navPanel = new NavPanel(controller); - leftPane = new VBox(navPanel, new SummaryTablePane(controller)); + groupTree = new GroupTree(controller); + hashHitList = new HashHitGroupList(controller); + + TabPane tabPane = new TabPane(groupTree, hashHitList); + + VBox.setVgrow(tabPane, Priority.ALWAYS); + leftPane = new VBox(tabPane, new SummaryTablePane(controller)); SplitPane.setResizableWithParent(leftPane, Boolean.FALSE); SplitPane.setResizableWithParent(groupPane, Boolean.TRUE); SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE); @@ -158,7 +167,7 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl splitPane.setDividerPositions(0.0, 1.0); ImageGalleryController.getDefault().setStacks(fullUIStack, centralStack); - ImageGalleryController.getDefault().setNavPanel(navPanel); + ImageGalleryController.getDefault().setShowTree(() -> tabPane.getSelectionModel().select(groupTree)); }); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java index 0d0b02bfbc..8ba8ac6af6 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -39,6 +39,8 @@ import java.util.stream.Stream; import javafx.application.Platform; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import static javafx.concurrent.Worker.State.CANCELLED; @@ -116,10 +118,13 @@ public class GroupManager { * --- current grouping/sorting attributes --- */ private volatile GroupSortBy sortBy = GroupSortBy.NONE; - private volatile DrawableAttribute groupBy = DrawableAttribute.PATH; - private volatile SortOrder sortOrder = SortOrder.ASCENDING; + + private final ReadOnlyObjectWrapper sortByProp = new ReadOnlyObjectWrapper<>(sortBy); + private final ReadOnlyObjectWrapper< DrawableAttribute> groupByProp = new ReadOnlyObjectWrapper<>(groupBy); + private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(sortOrder); + private final ReadOnlyDoubleWrapper regroupProgress = new ReadOnlyDoubleWrapper(); public void setDB(DrawableDB db) { @@ -424,24 +429,39 @@ public class GroupManager { return sortBy; } - public void setSortBy(GroupSortBy sortBy) { + void setSortBy(GroupSortBy sortBy) { this.sortBy = sortBy; + Platform.runLater(() -> sortByProp.set(sortBy)); + } + + public ReadOnlyObjectProperty getSortByProperty() { + return sortByProp.getReadOnlyProperty(); } public DrawableAttribute getGroupBy() { return groupBy; } - public void setGroupBy(DrawableAttribute groupBy) { + void setGroupBy(DrawableAttribute groupBy) { this.groupBy = groupBy; + Platform.runLater(() -> groupByProp.set(groupBy)); + } + + public ReadOnlyObjectProperty> getGroupByProperty() { + return groupByProp.getReadOnlyProperty(); } public SortOrder getSortOrder() { return sortOrder; } - public void setSortOrder(SortOrder sortOrder) { + void setSortOrder(SortOrder sortOrder) { this.sortOrder = sortOrder; + Platform.runLater(() -> sortOrderProp.set(sortOrder)); + } + + public ReadOnlyObjectProperty getSortOrderProperty() { + return sortOrderProp.getReadOnlyProperty(); } /** diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupComparators.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupComparators.java new file mode 100644 index 0000000000..c7bce311c4 --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupComparators.java @@ -0,0 +1,82 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-16 Basis Technology Corp. + * Contact: carrier sleuthkit 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 com.google.common.collect.ImmutableList; +import java.util.Comparator; +import java.util.function.Function; +import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; + +/** + * + */ +final class GroupComparators> implements Comparator { + + static final GroupComparators UNCATEGORIZED_COUNT = + new GroupComparators<>("Uncategorized Count", DrawableGroup::getUncategorizedCount, String::valueOf, false); + + static final GroupComparators ALPHABETICAL = + new GroupComparators<>("Group Name", DrawableGroup::getGroupByValueDislpayName, String::valueOf, false); + + static final GroupComparators HIT_COUNT = + new GroupComparators<>("Hit Count", DrawableGroup::getHashSetHitsCount, String::valueOf, true); + + static final GroupComparators FILE_COUNT = + new GroupComparators<>("Group Size", DrawableGroup::getSize, String::valueOf, true); + + static final GroupComparators HIT_FILE_RATIO = + new GroupComparators<>("Hit Density", DrawableGroup::getHashHitDensity, density -> String.format("%.2f", density) + "%", true); + + private final static ImmutableList> values = ImmutableList.of(UNCATEGORIZED_COUNT, ALPHABETICAL, HIT_COUNT, FILE_COUNT, HIT_FILE_RATIO); + + public static ImmutableList> getValues() { + return values; + } + + private final Function extractor; + private final Function valueFormatter; + private final boolean orderReveresed; + private final String displayName; + + private GroupComparators(String displayName, Function extractor, Function formatter, boolean defaultOrderReversed) { + this.displayName = displayName; + this.extractor = extractor; + this.orderReveresed = defaultOrderReversed; + this.valueFormatter = formatter; + } + + @Override + public int compare(DrawableGroup o1, DrawableGroup o2) { + int compareTo = extractor.apply(o1).compareTo(extractor.apply(o2)); + return orderReveresed ? -compareTo : compareTo; + } + + public String getDisplayName() { + return displayName; + } + + @Override + public String toString() { + return displayName; + } + + String getFormattedValueOfGroup(DrawableGroup group) { + return valueFormatter.apply(extractor.apply(group)); + } +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupListCell.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupListCell.java new file mode 100644 index 0000000000..8a56db05b3 --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupListCell.java @@ -0,0 +1,177 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 Basis Technology Corp. + * Contact: carrier sleuthkit 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.control.ListCell; +import javafx.scene.control.OverrunStyle; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javax.annotation.Nonnull; +import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; + +/** + * + */ +class GroupListCell extends ListCell { + + /** + * icon to use if this cell's TreeNode 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(GroupTreeCell.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/folder.png")); + + /** + * reference to group files listener that allows us to remove it from a + * group when a new group is assigned to this Cell + */ + private final InvalidationListener fileCountListener = (Observable o) -> { + final String text = getGroupName() + getCountsText(); + Platform.runLater(() -> { + setText(text); + setTooltip(new Tooltip(text)); + }); + }; + + /** + * reference to group seen listener that allows us to remove it from a group + * when a new group is assigned to this Cell + */ + private final InvalidationListener seenListener = (Observable o) -> { + final String style = getSeenStyleClass(); + Platform.runLater(() -> { + setStyle(style); + }); + }; + + private final ReadOnlyObjectProperty> sortOrder; + + GroupListCell(ReadOnlyObjectProperty> sortOrderProperty) { + + this.sortOrder = sortOrderProperty; + getStylesheets().add(GroupTreeCell.class.getResource("GroupTreeCell.css").toExternalForm()); + getStyleClass().add("groupTreeCell"); //reduce indent to 5, default is 10 which uses up a lot of space. + + //since end of path is probably more interesting put ellipsis at front + setTextOverrun(OverrunStyle.LEADING_ELLIPSIS); + Platform.runLater(() -> { + prefWidthProperty().bind(getListView().widthProperty().subtract(15)); + }); + + } + + /** + * {@inheritDoc } + */ + @Override + protected synchronized void updateItem(final DrawableGroup group, boolean empty) { + //if there was a previous group, remove the listeners + Optional.ofNullable(getItem()) + .ifPresent(oldGroup -> { + sortOrder.removeListener(fileCountListener); + oldGroup.fileIds().removeListener(fileCountListener); + oldGroup.seenProperty().removeListener(seenListener); + oldGroup.uncatCountProperty().removeListener(fileCountListener); + }); + + super.updateItem(group, empty); + + if (isNull(group) || empty) { + Platform.runLater(() -> { + setTooltip(null); + setText(null); + setGraphic(null); + setStyle(""); + }); + } else { + if (isNull(group)) { + final String text = getGroupName(); + //"dummy" group in file system tree <=> a folder with no drawables + Platform.runLater(() -> { + setTooltip(new Tooltip(text)); + setText(text); + setGraphic(new ImageView(EMPTY_FOLDER_ICON)); + setStyle(""); + }); + + } else { + //if number of files in this group changes (eg a file is recategorized), update counts via listener + group.fileIds().addListener(fileCountListener); + group.uncatCountProperty().addListener(fileCountListener); + sortOrder.addListener(fileCountListener); + //if the seen state of this group changes update its style + group.seenProperty().addListener(seenListener); + + //and use icon corresponding to group type + final Image icon = group.groupKey.getAttribute().getIcon(); + final String text = getGroupName() + getCountsText(); + final String style = getSeenStyleClass(); + Platform.runLater(() -> { + setTooltip(new Tooltip(text)); + setGraphic(new ImageView(icon)); + setText(text); + setStyle(style); + }); + } + } + } + + private String getGroupName() { + return Optional.ofNullable(getItem()) + .map(group -> group.getGroupByValueDislpayName()) + .orElse(""); + } + + /** + * return the styleClass to apply based on the assigned group's seen status + * + * @return the style class to apply + */ + @Nonnull + private String getSeenStyleClass() { + return Optional.ofNullable(getItem()) + .map(DrawableGroup::isSeen) + .map(seen -> seen ? "" : "-fx-font-weight:bold;") + .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 + */ + @Nonnull + private String getCountsText() { + + return Optional.ofNullable(getItem()) + .map(group -> + " (" + (sortOrder.get() == GroupComparators.ALPHABETICAL + ? group.getSize() + : sortOrder.get().getFormattedValueOfGroup(group)) + ")" + ).orElse(""); //if item is null or group is null + } +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTree.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTree.java new file mode 100644 index 0000000000..f2d6fc0e95 --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTree.java @@ -0,0 +1,149 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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 java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.collections.ListChangeListener; +import javafx.fxml.FXML; +import javafx.scene.control.SelectionModel; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.image.ImageView; +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; +import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; +import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; + +/** + * Shows path based groups as a tree and others kinds of groups as a flat list ( + * a tree with an invisible root and only one level of children). Shows controls + * to adjust the sorting only in flat list mode. + */ +final public class GroupTree extends NavPanel> { + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private final GroupTreeItem groupTreeRoot = new GroupTreeItem("", null, true); + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private final TreeView groupTree = new TreeView<>(groupTreeRoot); + + public GroupTree(ImageGalleryController controller) { + super(controller); + FXMLConstructor.construct(this, "NavPanel.fxml"); + } + + @FXML + @Override + void initialize() { + super.initialize(); + setText("All Groups"); + setGraphic(new ImageView("org/sleuthkit/autopsy/imagegallery/images/Folder-icon.png")); + + getBorderPane().setCenter(groupTree); + + //only show sorting controls if not grouping by path + BooleanBinding groupedByPath = Bindings.equal(getGroupManager().getGroupByProperty(), DrawableAttribute.PATH); + getToolBar().visibleProperty().bind(groupedByPath.not()); + getToolBar().managedProperty().bind(groupedByPath.not()); + + groupTree.setCellFactory(treeView -> new GroupTreeCell(getSortByBox().getSelectionModel().selectedItemProperty())); + groupTree.setShowRoot(false); + getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change change) -> { + while (change.next()) { + change.getAddedSubList().stream().forEach(this::insertGroup); + change.getRemoved().stream().forEach(this::removeFromTree); + } + }); + + for (DrawableGroup g : getGroupManager().getAnalyzedGroups()) { + insertGroup(g); + } + } + + /** + * Set the tree to the passed in group + * + * @param grouping + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + @Override + void setFocusedGroup(DrawableGroup grouping) { + final GroupTreeItem treeItemForGroup = groupTreeRoot.getTreeItemForPath(groupingToPath(grouping)); + + if (treeItemForGroup != null) { + groupTree.getSelectionModel().select(treeItemForGroup); + Platform.runLater(() -> { + int row = groupTree.getRow(treeItemForGroup); + if (row != -1) { + groupTree.scrollTo(row - 2); //put newly selected row 3 from the top + } + }); + } + } + + @Override + Function, DrawableGroup> getDataItemMapper() { + return treeItem -> treeItem.getValue().getGroup(); + } + + @Override + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + SelectionModel> getSelectionModel() { + return groupTree.getSelectionModel(); + } + + @Override + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + void applyGroupComparator() { + groupTreeRoot.resortChildren(getComparator()); + } + + @Override + GroupComparators getDefaultComparator() { + return GroupComparators.ALPHABETICAL; + } + + private void insertGroup(DrawableGroup g) { + groupTreeRoot.insert(groupingToPath(g), g, true); + } + + private void removeFromTree(DrawableGroup g) { + Optional.ofNullable(groupTreeRoot.getTreeItemForGroup(g)) + .ifPresent(GroupTreeItem::removeFromParent); + } + + private static List groupingToPath(DrawableGroup g) { + String path = g.groupKey.getValueDisplayName(); + if (g.groupKey.getAttribute() != DrawableAttribute.PATH) { + String stripStart = StringUtils.strip(path, "/"); + return Arrays.asList(stripStart); + } else { + String[] cleanPathTokens = StringUtils.stripStart(path, "/").split("/"); + return Arrays.asList(cleanPathTokens); + } + } +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeCell.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeCell.java index 70f1caf5ed..bf7425ec57 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeCell.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeCell.java @@ -40,7 +40,7 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; * TODO: we should use getStyleClass().add() rather than setStyle but it didn't * seem to work properly */ -class GroupTreeCell extends TreeCell { +class GroupTreeCell extends TreeCell { /** * icon to use if this cell's TreeNode doesn't represent a group but just a @@ -71,11 +71,9 @@ class GroupTreeCell extends TreeCell { setStyle(style); }); }; + private final ReadOnlyObjectProperty> sortOrder; - private final ReadOnlyObjectProperty> sortOrder; - - GroupTreeCell(ReadOnlyObjectProperty> sortOrderProperty) { - + GroupTreeCell(ReadOnlyObjectProperty> sortOrderProperty) { this.sortOrder = sortOrderProperty; getStylesheets().add(GroupTreeCell.class.getResource("GroupTreeCell.css").toExternalForm()); getStyleClass().add("groupTreeCell"); //reduce indent to 5, default is 10 which uses up a lot of space. @@ -92,12 +90,12 @@ class GroupTreeCell extends TreeCell { * {@inheritDoc } */ @Override - protected synchronized void updateItem(final TreeNode treeNode, boolean empty) { + protected synchronized void updateItem(final GroupTreeNode treeNode, boolean empty) { //if there was a previous group, remove the listeners Optional.ofNullable(getItem()) - .map(TreeNode::getGroup) + .map(GroupTreeNode::getGroup) .ifPresent(group -> { - sortOrder.removeListener(fileCountListener); + sortOrder.addListener(fileCountListener); group.fileIds().removeListener(fileCountListener); group.seenProperty().removeListener(seenListener); group.uncatCountProperty().removeListener(fileCountListener); @@ -159,7 +157,7 @@ class GroupTreeCell extends TreeCell { @Nonnull private String getSeenStyleClass() { return Optional.ofNullable(getItem()) - .map(TreeNode::getGroup) + .map(GroupTreeNode::getGroup) .map(DrawableGroup::isSeen) .map(seen -> seen ? "" : "-fx-font-weight:bold;") .orElse(""); //if item is null or group is null @@ -173,13 +171,12 @@ class GroupTreeCell extends TreeCell { */ @Nonnull private String getCountsText() { - return Optional.ofNullable(getItem()) - .filter(treeNode -> treeNode.getGroup() != null) - .map(item -> - sortOrder.get() == TreeNodeComparator.ALPHABETICAL - ? " (" + item.getSize() + ")" - : " (" + sortOrder.get().getFormattedValueOfTreeNode(item) + ")") - .orElse(""); //if item is null or group is null + .map(GroupTreeNode::getGroup) + .map(group -> + " (" + (sortOrder.get() == GroupComparators.ALPHABETICAL + ? group.getSize() + : sortOrder.get().getFormattedValueOfGroup(group)) + ")" + ).orElse(""); //if item is null or group is null } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeItem.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeItem.java index 4ac7f09323..2df2499518 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeItem.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeItem.java @@ -38,7 +38,7 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; * {@link GroupTreeCell}. Each GroupTreeItem has a TreeNode which has a path * segment and may or may not have a group */ -class GroupTreeItem extends TreeItem implements Comparable { +class GroupTreeItem extends TreeItem { static final Executor treeInsertTread = Executors.newSingleThreadExecutor(); @@ -64,12 +64,12 @@ class GroupTreeItem extends TreeItem implements Comparable comp; + private Comparator comp; - GroupTreeItem(String t, DrawableGroup g, Comparator comp, boolean expanded) { - super(new TreeNode(t, g)); - this.comp = comp; + GroupTreeItem(String t, DrawableGroup g, boolean expanded) { + super(new GroupTreeNode(t, g)); setExpanded(expanded); + comp = GroupComparators.ALPHABETICAL; } /** @@ -92,7 +92,7 @@ class GroupTreeItem extends TreeItem implements Comparable path, DrawableGroup g, Boolean tree) { + synchronized void insert(List path, DrawableGroup g, boolean tree) { if (tree) { // Are we at the end of the recursion? if (path.isEmpty()) { @@ -101,7 +101,7 @@ class GroupTreeItem extends TreeItem implements Comparable { - final GroupTreeItem newTreeItem = new GroupTreeItem(t, null, comp, false); + final GroupTreeItem newTreeItem = new GroupTreeItem(t, null, false); Platform.runLater(() -> { getChildren().add(newTreeItem); @@ -111,7 +111,7 @@ class GroupTreeItem extends TreeItem implements Comparable { - prefixTreeItem.insert(path.subList(1, path.size()), g, tree); + prefixTreeItem.insert(path.subList(1, path.size()), g, true); }); } @@ -119,25 +119,16 @@ class GroupTreeItem extends TreeItem implements Comparable { - final GroupTreeItem newTreeItem = new GroupTreeItem(t, g, comp, false); - newTreeItem.setExpanded(true); - + final GroupTreeItem newTreeItem = new GroupTreeItem(t, g, true); Platform.runLater(() -> { getChildren().add(newTreeItem); - if (comp != null) { - getChildren().sort(Comparator.comparing(TreeItem::getValue, comp)); - } + getChildren().sort(Comparator.comparing(treeItem -> treeItem.getValue().getGroup(), comp)); }); return newTreeItem; }); } } - @Override - public int compareTo(GroupTreeItem o) { - return comp.compare(this.getValue(), o.getValue()); - } - synchronized GroupTreeItem getTreeItemForPath(List path) { if (path.isEmpty()) { @@ -178,9 +169,9 @@ class GroupTreeItem extends TreeItem implements Comparable newComp) { + synchronized void resortChildren(Comparator newComp) { this.comp = newComp; - getChildren().sort(Comparator.comparing(TreeItem::getValue, comp)); + getChildren().sort(Comparator.comparing(treeItem -> treeItem.getValue().getGroup(), comp)); for (GroupTreeItem ti : childMap.values()) { ti.resortChildren(comp); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/TreeNode.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeNode.java similarity index 63% rename from ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/TreeNode.java rename to ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeNode.java index b6fed3df1b..8cb32336c2 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/TreeNode.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeNode.java @@ -23,7 +23,7 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; /** * */ -class TreeNode { +class GroupTreeNode { private final String path; private DrawableGroup group; @@ -36,7 +36,7 @@ class TreeNode { return group; } - TreeNode(String path, DrawableGroup group) { + GroupTreeNode(String path, DrawableGroup group) { this.path = path; this.group = group; } @@ -44,24 +44,4 @@ class TreeNode { void setGroup(DrawableGroup g) { group = g; } - - public String getGroupByValueDislpayName() { - return (group == null) ? "" : group.getGroupByValueDislpayName(); - } - - public synchronized int getSize() { - return (group == null) ? 0 : group.getSize(); - } - - public double getHashHitDensity() { - return (group == null) ? 0 : group.getHashHitDensity(); - } - - public synchronized long getHashSetHitsCount() { - return (group == null) ? 0 : group.getHashSetHitsCount(); - } - - public final synchronized long getUncategorizedCount() { - return (group == null) ? 0 : group.getUncategorizedCount(); - } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/HashHitGroupList.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/HashHitGroupList.java new file mode 100644 index 0000000000..1431e46cf7 --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/HashHitGroupList.java @@ -0,0 +1,96 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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 java.util.function.Function; +import javafx.collections.transformation.SortedList; +import javafx.fxml.FXML; +import javafx.scene.control.ListView; +import javafx.scene.control.SelectionModel; +import javafx.scene.image.ImageView; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; +import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; +import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; + +/** + * Shows only groups with hash hits in a flat list, with controls to adjust + * sorting of list. + */ +final public class HashHitGroupList extends NavPanel { + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private final ListView groupList = new ListView<>(); + + /** + * sorted list of groups, setting a new comparator on this changes the + * sorting in the ListView. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private SortedList sorted; + + public HashHitGroupList(ImageGalleryController controller) { + super(controller); + FXMLConstructor.construct(this, "NavPanel.fxml"); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + @Override + SelectionModel getSelectionModel() { + return groupList.getSelectionModel(); + } + + @Override + Function< DrawableGroup, DrawableGroup> getDataItemMapper() { + return Function.identity(); // this view already works with groups, so do nothing + } + + @Override + void applyGroupComparator() { + sorted.setComparator(getComparator()); + } + + @FXML + @Override + void initialize() { + super.initialize(); + + setText("Only Hash Hits"); + setGraphic(new ImageView("org/sleuthkit/autopsy/imagegallery/images/hashset_hits.png")); + + getBorderPane().setCenter(groupList); + sorted = getController().getGroupManager().getAnalyzedGroups().filtered((DrawableGroup t) -> t.getHashSetHitsCount() > 0).sorted(); + + groupList.setCellFactory(treeView -> new GroupListCell(getSortByBox().getSelectionModel().selectedItemProperty())); + + groupList.setItems(sorted); + + } + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + @Override + void setFocusedGroup(DrawableGroup grouping) { + groupList.getSelectionModel().select(grouping); + } + + @Override + GroupComparators getDefaultComparator() { + return GroupComparators.HIT_COUNT; + } +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/NavPanel.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/NavPanel.fxml index a4b88b0320..73e4b50f28 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/NavPanel.fxml +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/NavPanel.fxml @@ -4,96 +4,61 @@ - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - + + + + + + + - - - - -
-
-
-
+ + + + + + + + + + +
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/NavPanel.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/NavPanel.java index 941948b191..404bd7c704 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/NavPanel.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/NavPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,63 +19,37 @@ package org.sleuthkit.autopsy.imagegallery.gui.navpanel; import com.google.common.eventbus.Subscribe; -import java.util.Arrays; import java.util.Comparator; -import java.util.List; import java.util.Optional; -import javafx.application.Platform; -import javafx.collections.ListChangeListener; +import java.util.function.Function; import javafx.fxml.FXML; import javafx.scene.control.ComboBox; import javafx.scene.control.RadioButton; -import javafx.scene.control.SelectionMode; +import javafx.scene.control.SelectionModel; import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; import javafx.scene.control.ToggleGroup; -import javafx.scene.control.TreeItem; -import javafx.scene.control.TreeView; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; -import org.apache.commons.lang3.StringUtils; +import javafx.scene.control.ToolBar; +import javafx.scene.layout.BorderPane; import org.sleuthkit.autopsy.coreutils.ThreadConfined; -import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType; -import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; -import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; +import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; /** - * Display two trees. one shows all folders (groups) and calls out folders with - * images. the user can select folders with images to see them in the main - * GroupPane The other shows folders with hash set hits. + * Base class for Tabs in the left hand Navigation/Context area. */ -final public class NavPanel extends VBox { +abstract class NavPanel extends Tab { @FXML - private ComboBox> sortByBox; + private BorderPane borderPane; @FXML - private TabPane navTabPane; + private ToolBar toolBar; @FXML - private TreeView navTree; - - @FXML - private AnchorPane hashAnchor; - - @FXML - private AnchorPane navAnchor; - @FXML - private Tab hashTab; - - @FXML - private Tab navTab; - - private final GroupTreeItem navTreeRoot = new GroupTreeItem("", null, TreeNodeComparator.ALPHABETICAL, true); - private final GroupTreeItem hashTreeRoot = new GroupTreeItem("", null, TreeNodeComparator.HIT_COUNT, true); + private ComboBox> sortByBox; @FXML private RadioButton ascRadio; @@ -87,161 +61,155 @@ final public class NavPanel extends VBox { private RadioButton descRadio; private final ImageGalleryController controller; + private final GroupManager groupManager; + private final CategoryManager categoryManager; - private TreeNodeComparator hashSortOrder = TreeNodeComparator.HIT_COUNT; - - public NavPanel(ImageGalleryController controller) { + NavPanel(ImageGalleryController controller) { this.controller = controller; - FXMLConstructor.construct(this, "NavPanel.fxml"); - } - - @Subscribe - public void handleCategoryChange(CategoryManager.CategoryChangeEvent event) { - resortHashTree(); + this.groupManager = controller.getGroupManager(); + this.categoryManager = controller.getCategoryManager(); } @FXML void initialize() { - assert hashTab != null : "fx:id=\"hashTab\" was not injected: check your FXML file 'NavPanel.fxml'."; - assert navTab != null : "fx:id=\"navTab\" was not injected: check your FXML file 'NavPanel.fxml'."; - assert navTabPane != null : "fx:id=\"navTabPane\" was not injected: check your FXML file 'NavPanel.fxml'."; - assert navTree != null : "fx:id=\"navTree\" was not injected: check your FXML file 'NavPanel.fxml'."; + assert borderPane != null : "fx:id=\"borderPane\" was not injected: check your FXML file 'NavPanel.fxml'."; + assert toolBar != null : "fx:id=\"toolBar\" was not injected: check your FXML file 'NavPanel.fxml'."; assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'NavPanel.fxml'."; + assert ascRadio != null : "fx:id=\"ascRadio\" was not injected: check your FXML file 'NavPanel.fxml'."; + assert orderGroup != null : "fx:id=\"orderGroup\" was not injected: check your FXML file 'NavPanel.fxml'."; + assert descRadio != null : "fx:id=\"descRadio\" was not injected: check your FXML file 'NavPanel.fxml'."; - VBox.setVgrow(this, Priority.ALWAYS); - - sortByBox.getItems().setAll(TreeNodeComparator.getValues()); - sortByBox.getSelectionModel().select(TreeNodeComparator.ALPHABETICAL); - + sortByBox.getItems().setAll(GroupComparators.getValues()); + sortByBox.getSelectionModel().select(getDefaultComparator()); + orderGroup.selectedToggleProperty().addListener(order -> sortGroups()); sortByBox.getSelectionModel().selectedItemProperty().addListener(observable -> { - - if (sortByBox.getSelectionModel().getSelectedItem() == TreeNodeComparator.UNCATEGORIZED_COUNT) { - controller.getCategoryManager().registerListener(NavPanel.this); + sortGroups(); + //only need to listen to changes in category if we are sorting by/ showing the uncategorized count + if (sortByBox.getSelectionModel().getSelectedItem() == GroupComparators.UNCATEGORIZED_COUNT) { + categoryManager.registerListener(NavPanel.this); } else { - controller.getCategoryManager().unregisterListener(NavPanel.this); + categoryManager.unregisterListener(NavPanel.this); } - resortHashTree(); - }); - orderGroup.selectedToggleProperty().addListener(observable -> resortHashTree()); - navTree.setCellFactory(treeView -> new GroupTreeCell(sortByBox.getSelectionModel().selectedItemProperty())); - navTree.setShowRoot(false); - navTree.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); - navTree.getSelectionModel().selectedItemProperty().addListener(o -> updateControllersGroup()); - - navTree.setRoot(navTreeRoot); - navTabPane.getSelectionModel().selectedItemProperty().addListener(observable -> { - Tab selectedTab = navTabPane.getSelectionModel().getSelectedItem(); - if (selectedTab == navTab) { - hashSortOrder = sortByBox.getSelectionModel().getSelectedItem(); - sortByBox.getSelectionModel().select(TreeNodeComparator.ALPHABETICAL); - navTree.setRoot(navTreeRoot); - hashAnchor.getChildren().clear(); - navAnchor.getChildren().add(navTree); - } else if (selectedTab == hashTab) { - sortByBox.getSelectionModel().select(hashSortOrder); - navTree.setRoot(hashTreeRoot); - navAnchor.getChildren().clear(); - hashAnchor.getChildren().add(navTree); - resortHashTree(); - } - }); - - controller.getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change change) -> { - while (change.next()) { - change.getAddedSubList().stream().forEach(this::insertGroup); - change.getRemoved().stream().forEach(this::removeFromTree); - } }); + //keep selection in sync with controller controller.viewState().addListener(observable -> { Optional.ofNullable(controller.viewState().get()) .map(GroupViewState::getGroup) .ifPresent(this::setFocusedGroup); }); - for (DrawableGroup g : controller.getGroupManager().getAnalyzedGroups()) { - insertGroup(g); - } + getSelectionModel().selectedItemProperty().addListener(o -> updateControllersGroup()); } - private void insertGroup(DrawableGroup g) { - navTreeRoot.insert(groupingToPath(g), g, true); - if (g.getHashSetHitsCount() > 0) { - hashTreeRoot.insert(groupingToPath(g), g, false); - } + /** + * @return the default comparator used by this "view" to sort groups + */ + abstract GroupComparators getDefaultComparator(); + + @Subscribe + public void handleCategoryChange(CategoryManager.CategoryChangeEvent event) { + sortGroups(); } - private void updateControllersGroup() { - Optional.ofNullable(navTree.getSelectionModel().getSelectedItem()) - .map(TreeItem::getValue) - .map(TreeNode::getGroup) + /** + * @return the a comparator that will enforce the currently selected sorting + * options. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + Comparator getComparator() { + Comparator comparator = sortByBox.getSelectionModel().getSelectedItem(); + return (orderGroup.getSelectedToggle() == ascRadio) + ? comparator + : comparator.reversed(); + } + + /** + * notify controller about group selection in this view + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + void updateControllersGroup() { + Optional.ofNullable(getSelectionModel().getSelectedItem()) + .map(getDataItemMapper()) .ifPresent(group -> controller.advance(GroupViewState.tile(group), false)); } - @ThreadConfined(type = ThreadType.JFX) - private void resortHashTree() { - Comparator treeNodeComparator = sortByBox.getSelectionModel().getSelectedItem(); - if (orderGroup.getSelectedToggle() == descRadio) { - treeNodeComparator = treeNodeComparator.reversed(); - } - TreeItem selectedItem = navTree.getSelectionModel().getSelectedItem(); - hashTreeRoot.resortChildren(treeNodeComparator); - navTree.getSelectionModel().select(selectedItem); + /** + * Sort the groups in this view according to the currently selected sorting + * options. Attempts to maintain selection. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + void sortGroups() { + X selectedItem = getSelectionModel().getSelectedItem(); + applyGroupComparator(); + Optional.ofNullable(selectedItem) + .map(getDataItemMapper()) + .ifPresent(this::setFocusedGroup); } /** - * Set the tree to the passed in group - * - * @param grouping + * @return a function that maps the "native" data type of this view to a + * DrawableGroup */ - @ThreadConfined(type = ThreadType.JFX) - private void setFocusedGroup(DrawableGroup grouping) { - final GroupTreeItem treeItemForGroup = ((GroupTreeItem) navTree.getRoot()).getTreeItemForPath(groupingToPath(grouping)); + abstract Function getDataItemMapper(); - if (treeItemForGroup != null) { - navTree.getSelectionModel().select(treeItemForGroup); - Platform.runLater(() -> { - int row = navTree.getRow(treeItemForGroup); - if (row != -1) { - navTree.scrollTo(row - 2); //put newly selected row 3 from the top - } - }); - } - } - - private List groupingToPath(DrawableGroup g) { - String path = g.groupKey.getValueDisplayName(); - if (g.groupKey.getAttribute() != DrawableAttribute.PATH - || navTabPane.getSelectionModel().getSelectedItem() == hashTab) { - String stripStart = StringUtils.strip(path, "/"); - return Arrays.asList(stripStart); - } else { - String[] cleanPathTokens = StringUtils.stripStart(path, "/").split("/"); - return Arrays.asList(cleanPathTokens); - } - } + /** + * Apply the currently selected sorting options. + */ + abstract void applyGroupComparator(); /** * - * @param g the value of g - * @param treeRoot the value of treeRoot + * @return get the selection model */ - private void removeFromTree(DrawableGroup g) { - GroupTreeItem treeItemForGroup = navTreeRoot.getTreeItemForGroup(g); - if (treeItemForGroup != null) { - treeItemForGroup.removeFromParent(); - } + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + abstract SelectionModel getSelectionModel(); - treeItemForGroup = hashTreeRoot.getTreeItemForGroup(g); - if (treeItemForGroup != null) { - treeItemForGroup.removeFromParent(); - } + /** + * attempt to set the given group as the selected/focused group in this + * view. + * + * @param grouping the grouping to attempt to select + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + abstract void setFocusedGroup(DrawableGroup grouping); + + ////boring getters + BorderPane getBorderPane() { + return borderPane; } - public void showTree() { - Platform.runLater(() -> { - navTabPane.getSelectionModel().select(navTab); - }); + ToolBar getToolBar() { + return toolBar; } + + ComboBox> getSortByBox() { + return sortByBox; + } + + RadioButton getAscRadio() { + return ascRadio; + } + + ToggleGroup getOrderGroup() { + return orderGroup; + } + + RadioButton getDescRadio() { + return descRadio; + } + + ImageGalleryController getController() { + return controller; + } + + GroupManager getGroupManager() { + return groupManager; + } + + CategoryManager getCategoryManager() { + return categoryManager; + } + } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/TreeNodeComparator.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/TreeNodeComparator.java deleted file mode 100644 index 2b0fd53075..0000000000 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/TreeNodeComparator.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013-16 Basis Technology Corp. - * Contact: carrier sleuthkit 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 com.google.common.collect.ImmutableList; -import java.util.Comparator; -import java.util.function.Function; - -/** - * - */ -final class TreeNodeComparator> implements Comparator { - - static final TreeNodeComparator UNCATEGORIZED_COUNT = - new TreeNodeComparator<>("Uncategorized Count", TreeNode::getUncategorizedCount, String::valueOf, false); - - static final TreeNodeComparator ALPHABETICAL = - new TreeNodeComparator<>("Group Name", TreeNode::getGroupByValueDislpayName, String::valueOf, false); - - static final TreeNodeComparator HIT_COUNT = - new TreeNodeComparator<>("Hit Count", TreeNode::getHashSetHitsCount, String::valueOf, true); - - static final TreeNodeComparator FILE_COUNT = - new TreeNodeComparator<>("Group Size", TreeNode::getSize, String::valueOf, true); - - static final TreeNodeComparator HIT_FILE_RATIO = - new TreeNodeComparator<>("Hit Density", (treeNode) -> treeNode.getHashHitDensity(), density -> String.format("%.2f", density), true); - - private final static ImmutableList> values = ImmutableList.of(UNCATEGORIZED_COUNT, ALPHABETICAL, HIT_COUNT, FILE_COUNT, HIT_FILE_RATIO); - - public static ImmutableList> getValues() { - return values; - } - - private final Function extractor; - private final Function valueFormatter; - private final boolean orderReveresed; - private final String displayName; - - private TreeNodeComparator(String displayName, Function extractor, Function formatter, boolean defaultOrderReversed) { - this.displayName = displayName; - this.extractor = extractor; - this.orderReveresed = defaultOrderReversed; - this.valueFormatter = formatter; - } - - @Override - public int compare(TreeNode o1, TreeNode o2) { - int compareTo = extractor.apply(o1).compareTo(extractor.apply(o2)); - return orderReveresed ? -compareTo : compareTo; - } - - public String getDisplayName() { - return displayName; - } - - @Override - public String toString() { - return displayName; - } - - String getFormattedValueOfTreeNode(TreeNode group) { - return valueFormatter.apply(extractor.apply(group)); - } - -}