diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index 3fa6b8a2b4..52d2f8d240 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -634,8 +634,8 @@ public final class DrawableDB { static private String getGroupIdQuery(GroupKey groupKey) { // query to find the group id from attribute/value - return String.format("( SELECT group_id FROM " + GROUPS_TABLENAME - + " WHERE attribute = \'%s\' AND value = \'%s\' AND data_source_obj_id = %d)", + return String.format(" SELECT group_id FROM " + GROUPS_TABLENAME + + " WHERE attribute = \'%s\' AND value = \'%s\' AND data_source_obj_id = %d", groupKey.getAttribute().attrName.toString(), groupKey.getValueDisplayName(), (groupKey.getAttribute() == DrawableAttribute.PATH) ? groupKey.getDataSourceObjId() : 0); @@ -685,7 +685,8 @@ public final class DrawableDB { try { String groupSeenQueryStmt = "COUNT(*) as count FROM " + GROUPS_SEEN_TABLENAME - + " WHERE group_id in ( " + getGroupIdQuery(groupKey) + ")" + + " WHERE seen = 1 " + + " AND group_id in ( " + getGroupIdQuery(groupKey) + ")" + (examinerId > 0 ? " AND examiner_id = " + examinerId : "");// query to find the group id from attribute/value tskCase.getCaseDbAccessManager().select(groupSeenQueryStmt, queryResultProcessor); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java index 64a73d02b8..224ac378c6 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java @@ -26,6 +26,7 @@ import java.util.logging.Level; import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; import javafx.beans.binding.IntegerBinding; +import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyLongWrapper; @@ -165,8 +166,8 @@ public class DrawableGroup implements Comparable { return seen.get(); } - public ReadOnlyBooleanWrapper seenProperty() { - return seen; + public ReadOnlyBooleanProperty seenProperty() { + return seen.getReadOnlyProperty(); } @Subscribe 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 e48d4990cc..fa40fdb66a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -42,10 +42,12 @@ import java.util.TreeSet; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.regex.Pattern; import java.util.stream.Collectors; import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; @@ -126,6 +128,7 @@ public class GroupManager { private final ReadOnlyObjectWrapper< DrawableAttribute> groupByProp = new ReadOnlyObjectWrapper<>(DrawableAttribute.PATH); private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING); private final ReadOnlyObjectWrapper dataSourceProp = new ReadOnlyObjectWrapper<>(null);//null indicates all datasources + private final ReadOnlyBooleanWrapper collaborativeModeProp = new ReadOnlyBooleanWrapper(false); private final GroupingService regrouper; @@ -249,15 +252,15 @@ public class GroupManager { Examiner examiner = controller.getSleuthKitCase().getCurrentExaminer(); getDrawableDB().markGroupSeen(group.getGroupKey(), seen, examiner.getId()); group.setSeen(seen); - updateUnSeenGroups(group, seen); + updateUnSeenGroups(group); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error marking group as seen", ex); //NON-NLS } }); } - synchronized private void updateUnSeenGroups(DrawableGroup group, boolean seen) { - if (seen) { + synchronized private void updateUnSeenGroups(DrawableGroup group) { + if (group.isSeen()) { unSeenGroups.removeAll(group); } else if (unSeenGroups.contains(group) == false) { unSeenGroups.add(group); @@ -580,8 +583,8 @@ public class GroupManager { Set fileIDs = getFileIDsInGroup(groupKey); if (Objects.nonNull(fileIDs)) { - Examiner examiner = controller.getSleuthKitCase().getCurrentExaminer(); - final boolean groupSeen = getDrawableDB().isGroupSeenByExaminer(groupKey, examiner.getId()); + long examinerID = collaborativeModeProp.get() ? -1 : controller.getSleuthKitCase().getCurrentExaminer().getId(); + final boolean groupSeen = getDrawableDB().isGroupSeenByExaminer(groupKey, examinerID); DrawableGroup group; if (groupMap.containsKey(groupKey)) { @@ -598,10 +601,9 @@ public class GroupManager { analyzedGroups.add(group); sortAnalyzedGroups(); } - updateUnSeenGroups(group, groupSeen); + updateUnSeenGroups(group); return group; - } } catch (TskCoreException ex) { logger.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.toString() + " = " + groupKey.getValue(), ex); //NON-NLS @@ -634,6 +636,29 @@ public class GroupManager { } } + synchronized public void setCollaborativeMode(Boolean newValue) { + collaborativeModeProp.set(newValue); + analyzedGroups.forEach(group -> { + try { + boolean groupSeenByExaminer = getDrawableDB().isGroupSeenByExaminer( + group.getGroupKey(), + newValue ? -1 : controller.getSleuthKitCase().getCurrentExaminer().getId() + ); + group.setSeen(groupSeenByExaminer); + updateUnSeenGroups(group); + if (group.isSeen()) { + unSeenGroups.removeAll(group); + } else if (unSeenGroups.contains(group) == false) { + unSeenGroups.add(group); + } + + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error checking seen state of group.", ex); + } + }); + sortUnseenGroups(); + } + /** * Task to query database for files in sorted groups and build * DrawableGroups for them. diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml index ec6e539f87..fc286f3c7b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml @@ -2,6 +2,7 @@ + @@ -11,6 +12,7 @@ + @@ -18,7 +20,7 @@ - +
@@ -29,7 +31,7 @@ diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java index d730f8346a..faec0b768d 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java @@ -57,7 +57,9 @@ import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Bounds; +import javafx.scene.Cursor; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; @@ -85,6 +87,7 @@ import static javafx.scene.input.KeyCode.RIGHT; import static javafx.scene.input.KeyCode.UP; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Border; import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderStroke; @@ -95,6 +98,7 @@ import javafx.scene.layout.HBox; import javafx.scene.paint.Color; import javafx.util.Duration; import javax.swing.SwingUtilities; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.GridCell; import org.controlsfx.control.GridView; @@ -135,9 +139,8 @@ import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; import org.sleuthkit.datamodel.TskCoreException; /** - * A GroupPane displays the contents of a {@link DrawableGroup}. It supports - * both a {@link GridView} based view and a {@link SlideShowView} view by - * swapping out its internal components. + * A GroupPane displays the contents of a DrawableGroup. It supports both + * GridView and SlideShowView modes by swapping out its internal components. * * * TODO: Extract the The GridView instance to a separate class analogous to the @@ -150,27 +153,21 @@ import org.sleuthkit.datamodel.TskCoreException; public class GroupPane extends BorderPane { private static final Logger logger = Logger.getLogger(GroupPane.class.getName()); - private final ListeningExecutorService exec = TaskUtils.getExecutorForClass(GroupPane.class); private static final BorderWidths BORDER_WIDTHS_2 = new BorderWidths(2); private static final CornerRadii CORNER_RADII_2 = new CornerRadii(2); private static final DropShadow DROP_SHADOW = new DropShadow(10, Color.BLUE); - private static final Timeline flashAnimation = new Timeline(new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 1, Interpolator.LINEAR)), + private static final Timeline flashAnimation = new Timeline( + new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 1, Interpolator.LINEAR)), new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 15, Interpolator.LINEAR)) ); - private final FileIDSelectionModel selectionModel; - private static final List categoryKeyCodes = Arrays.asList(KeyCode.NUMPAD0, KeyCode.NUMPAD1, KeyCode.NUMPAD2, KeyCode.NUMPAD3, KeyCode.NUMPAD4, KeyCode.NUMPAD5, KeyCode.DIGIT0, KeyCode.DIGIT1, KeyCode.DIGIT2, KeyCode.DIGIT3, KeyCode.DIGIT4, KeyCode.DIGIT5); - private final Back backAction; - - private final Forward forwardAction; - @FXML private Button undoButton; @FXML @@ -178,13 +175,10 @@ public class GroupPane extends BorderPane { @FXML private SplitMenuButton catSelectedSplitMenu; - @FXML private SplitMenuButton tagSelectedSplitMenu; - @FXML private ToolBar headerToolBar; - @FXML private ToggleButton cat0Toggle; @FXML @@ -201,26 +195,25 @@ public class GroupPane extends BorderPane { @FXML private SegmentedButton segButton; - private SlideShowView slideShowPane; - @FXML private ToggleButton slideShowToggle; - - @FXML - private GridView gridView; - @FXML private ToggleButton tileToggle; + private SlideShowView slideShowPane; + + @FXML + private GridView gridView; @FXML private Button nextButton; - + @FXML + private AnchorPane nextButtonPane; + @FXML + private CheckBox seenByOtherExaminersCheckBox; @FXML private Button backButton; - @FXML private Button forwardButton; - @FXML private Label groupLabel; @FXML @@ -237,30 +230,27 @@ public class GroupPane extends BorderPane { @FXML private HBox catSplitMenuContainer; - private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler(); - - private final NextUnseenGroup nextGroupAction; + private final ListeningExecutorService exec = TaskUtils.getExecutorForClass(GroupPane.class); private final ImageGalleryController controller; - private ContextMenu contextMenu; - + private final FileIDSelectionModel selectionModel; private Integer selectionAnchorIndex; + private final UndoAction undoAction; private final RedoAction redoAction; + private final Back backAction; + private final Forward forwardAction; + private final NextUnseenGroup nextGroupAction; - GroupViewMode getGroupViewMode() { - return groupViewMode.get(); - } + private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler(); - /** - * the current GroupViewMode of this GroupPane - */ + private ContextMenu contextMenu; + + /** the current GroupViewMode of this GroupPane */ private final SimpleObjectProperty groupViewMode = new SimpleObjectProperty<>(GroupViewMode.TILE); - /** - * the grouping this pane is currently the view for - */ + /** the grouping this pane is currently the view for */ private final ReadOnlyObjectWrapper grouping = new ReadOnlyObjectWrapper<>(); /** @@ -294,6 +284,10 @@ public class GroupPane extends BorderPane { FXMLConstructor.construct(this, "GroupPane.fxml"); //NON-NLS } + GroupViewMode getGroupViewMode() { + return groupViewMode.get(); + } + @ThreadConfined(type = ThreadType.JFX) public void activateSlideShowViewer(Long slideShowFileID) { groupViewMode.set(GroupViewMode.SLIDE_SHOW); @@ -340,7 +334,9 @@ public class GroupPane extends BorderPane { } /** - * create the string to display in the group header + * Create the string to display in the group header. + * + * @return The string to display in the group header. */ @NbBundle.Messages({"# {0} - default group name", "# {1} - hashset hits count", @@ -391,19 +387,20 @@ public class GroupPane extends BorderPane { "GroupPane.catContainerLabel.displayText=Categorize Selected File:", "GroupPane.catHeadingLabel.displayText=Category:"}) void initialize() { - assert cat0Toggle != null : "fx:id=\"cat0Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat1Toggle != null : "fx:id=\"cat1Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat2Toggle != null : "fx:id=\"cat2Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat3Toggle != null : "fx:id=\"cat3Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat4Toggle != null : "fx:id=\"cat4Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat5Toggle != null : "fx:id=\"cat5Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; + assert cat0Toggle != null : "fx:id=\"cat0Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat1Toggle != null : "fx:id=\"cat1Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat2Toggle != null : "fx:id=\"cat2Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat3Toggle != null : "fx:id=\"cat3Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat4Toggle != null : "fx:id=\"cat4Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat5Toggle != null : "fx:id=\"cat5Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; assert gridView != null : "fx:id=\"tilePane\" was not injected: check your FXML file 'GroupPane.fxml'."; - assert catSelectedSplitMenu != null : "fx:id=\"grpCatSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert tagSelectedSplitMenu != null : "fx:id=\"grpTagSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert headerToolBar != null : "fx:id=\"headerToolBar\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert segButton != null : "fx:id=\"previewList\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert slideShowToggle != null : "fx:id=\"segButton\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert tileToggle != null : "fx:id=\"tileToggle\" was not injected: check your FXML file 'GroupHeader.fxml'."; + assert catSelectedSplitMenu != null : "fx:id=\"grpCatSplitMenu\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert tagSelectedSplitMenu != null : "fx:id=\"grpTagSplitMenu\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert headerToolBar != null : "fx:id=\"headerToolBar\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert segButton != null : "fx:id=\"previewList\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert slideShowToggle != null : "fx:id=\"segButton\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert tileToggle != null : "fx:id=\"tileToggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert seenByOtherExaminersCheckBox != null : "fx:id=\"seenByOtherExaminersCheckBox\" was not injected: check your FXML file 'GroupPane.fxml'."; for (DhsImageCategory cat : DhsImageCategory.values()) { ToggleButton toggleForCategory = getToggleForCategory(cat); @@ -530,6 +527,16 @@ public class GroupPane extends BorderPane { } }); + seenByOtherExaminersCheckBox.selectedProperty().addListener((observable, oldValue, newValue) -> { + nextButtonPane.setDisable(true); + nextButtonPane.setCursor(Cursor.WAIT); + exec.submit(() -> controller.getGroupManager().setCollaborativeMode(newValue)) + .addListener(() -> { + nextButtonPane.setDisable(false); + nextButtonPane.setCursor(Cursor.DEFAULT); + }, Platform::runLater); + }); + //listen to tile selection and make sure it is visible in scroll area selectionModel.lastSelectedProperty().addListener((observable, oldFileID, newFileId) -> { if (groupViewMode.get() == GroupViewMode.SLIDE_SHOW @@ -607,7 +614,7 @@ public class GroupPane extends BorderPane { * assigns a grouping for this pane to represent and initializes grouping * specific properties and listeners * - * @param grouping the new grouping assigned to this group + * @param newViewState */ void setViewState(GroupViewState newViewState) { @@ -894,7 +901,7 @@ public class GroupPane extends BorderPane { if (t.getClickCount() == 1) { selectAllFiles(); } - if (selectionModel.getSelected().isEmpty() == false) { + if (isNotEmpty(selectionModel.getSelected())) { if (contextMenu == null) { contextMenu = buildContextMenu(); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java index d21b76d018..72f602d06a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java @@ -173,7 +173,7 @@ class GroupCellFactory { private final InvalidationListener groupListener = new GroupListener<>(this); /** - * reference to group files listener that allows us to remove it from a + * Reference to group files listener that allows us to remove it from a * group when a new group is assigned to this Cell */ @Override