diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java index 37be779969..cb912c2a79 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2013-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,6 +37,7 @@ public class ImageGalleryPreferences { */ private static final String ENABLED_BY_DEFAULT = "enabled_by_default"; //NON-NLS private static final String GROUP_CATEGORIZATION_WARNING_DISABLED = "group_categorization_warning_disabled"; //NON-NLS + private static final String MULTI_USER_CASE_INFO_DIALOG_DISABLED = "multi_user_case_info_dialog_disabled"; //NON-NLS /** * Return setting of whether Image Analyzer should be automatically enabled @@ -46,7 +47,7 @@ public class ImageGalleryPreferences { * @return true if new cases should have image analyzer enabled. */ public static boolean isEnabledByDefault() { - return preferences.getBoolean(ENABLED_BY_DEFAULT, true); + return preferences.getBoolean(ENABLED_BY_DEFAULT, true); } public static void setEnabledByDefault(boolean b) { @@ -68,6 +69,21 @@ public class ImageGalleryPreferences { preferences.putBoolean(GROUP_CATEGORIZATION_WARNING_DISABLED, b); } + /** + * Return whether the dialog describing multi user case updating is + * disabled. + * + * @return true if the dialog is disabled. + */ + public static boolean isMultiUserCaseInfoDialogDisabled() { + final boolean aBoolean = preferences.getBoolean(MULTI_USER_CASE_INFO_DIALOG_DISABLED, false); + return aBoolean; + } + + public static void setMultiUserCaseInfoDialogDisabled(boolean b) { + preferences.putBoolean(MULTI_USER_CASE_INFO_DIALOG_DISABLED, b); + } + static void addChangeListener(PreferenceChangeListener l) { preferences.addPreferenceChangeListener(l); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java index 3794c69e83..81431b771e 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java @@ -18,14 +18,25 @@ */ package org.sleuthkit.autopsy.imagegallery.actions; +import com.google.common.util.concurrent.ListenableFuture; +import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; import java.awt.Component; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import static java.util.concurrent.Executors.newSingleThreadExecutor; import java.util.logging.Level; +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javax.annotation.concurrent.ThreadSafe; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JMenuItem; -import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; import org.openide.awt.ActionReferences; @@ -34,15 +45,19 @@ import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.util.actions.CallableSystemAction; -import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.core.Installer; import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule; +import org.sleuthkit.autopsy.imagegallery.ImageGalleryPreferences; import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; +import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils; +import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; +import static org.sleuthkit.autopsy.imagegallery.utils.TaskUtils.addFXCallback; import org.sleuthkit.datamodel.TskCoreException; @ActionID(category = "Tools", id = "org.sleuthkit.autopsy.imagegallery.OpenAction") @@ -119,9 +134,13 @@ public final class OpenAction extends CallableSystemAction { } @Override - @NbBundle.Messages({"OpenAction.dialogTitle=Image Gallery"}) + @NbBundle.Messages({"OpenAction.dialogTitle=Image Gallery", + "OpenAction.multiUserDialog.Header=Multi-user Image Gallery", + "OpenAction.multiUserDialog.ContentText=The Image Gallery updates itself differently for multi-user cases than single user cases. Notably:\n\n" + + "If your computer is analyzing a data source, then you will get real-time Image Gallery updates as files are analyzed (hashed, EXIF, etc.). This is the same behavior as a single-user case.\n\n" + + "If another computer in your multi-user cluster is analyzing a data source, you will get updates about files on that data source only when you launch Image Gallery, which will cause the local database to be rebuilt based on results from other nodes.", + "OpenAction.multiUserDialog.checkBox.text=Don't show this message again."}) public void performAction() { - //check case final Case currentCase; try { @@ -130,57 +149,103 @@ public final class OpenAction extends CallableSystemAction { logger.log(Level.SEVERE, "Exception while getting open case.", ex); return; } - + ImageGalleryController controller; try { - ImageGalleryController controller = ImageGalleryModule.getController(); - if (controller.isDataSourcesTableStale()) { - //drawable db is stale, ask what to do - int answer = JOptionPane.showConfirmDialog( - WindowManager.getDefault().getMainWindow(), - Bundle.OpenAction_stale_confDlg_msg(), - Bundle.OpenAction_stale_confDlg_title(), - JOptionPane.YES_NO_CANCEL_OPTION, - JOptionPane.WARNING_MESSAGE); - - switch (answer) { - case JOptionPane.YES_OPTION: - /* For a single-user case, we favor user experience, and - * rebuild the database as soon as Image Gallery is - * enabled for the case. For a multi-user case, we favor - * overall performance and user experience, not every - * user may want to review images, so we rebuild the - * database only when a user launches Image Gallery. - */ - if (currentCase.getCaseType() == Case.CaseType.SINGLE_USER_CASE) { - /* - * Turning listening off is necessary in order to - * invoke the listener that will call - * controller.rebuildDB(); - */ - controller.setListeningEnabled(false); - controller.setListeningEnabled(true); - } else { - controller.rebuildDB(); - } - ImageGalleryTopComponent.openTopComponent(); - break; - - case JOptionPane.NO_OPTION: { - ImageGalleryTopComponent.openTopComponent(); - } - break; - case JOptionPane.CANCEL_OPTION: - break; //do nothing - } - } else { - //drawable db is not stale, just open it - ImageGalleryTopComponent.openTopComponent(); - } - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS + controller = ImageGalleryModule.getController(); + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting ImageGalleryController for current case.", ex); + return; } + Platform.runLater(() -> { + if (currentCase.getCaseType() == Case.CaseType.MULTI_USER_CASE + && ImageGalleryPreferences.isMultiUserCaseInfoDialogDisabled() == false) { + Alert dialog = new Alert(Alert.AlertType.INFORMATION); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setResizable(true); + dialog.setTitle(Bundle.OpenAction_dialogTitle()); + dialog.setHeaderText(Bundle.OpenAction_multiUserDialog_Header()); + + Label label = new Label(Bundle.OpenAction_multiUserDialog_ContentText()); + label.setMaxWidth(450); + label.setWrapText(true); + CheckBox dontShowAgainCheckBox = new CheckBox(Bundle.OpenAction_multiUserDialog_checkBox_text()); + dialog.getDialogPane().setContent(new VBox(10, label, dontShowAgainCheckBox)); + GuiUtils.setDialogIcons(dialog); + + dialog.showAndWait(); + + if (dialog.getResult() == ButtonType.OK && dontShowAgainCheckBox.isSelected()) { + ImageGalleryPreferences.setMultiUserCaseInfoDialogDisabled(true); + } + } + + checkDBStale(controller); + }); + } + + private void checkDBStale(ImageGalleryController controller) { + //check if db is stale on throw away bg thread and then react back on jfx thread. + ListenableFuture staleFuture = TaskUtils.getExecutorForClass(OpenAction.class) + .submit(controller::isDataSourcesTableStale); + addFXCallback(staleFuture, + dbIsStale -> { + //back on fx thread. + if (false == dbIsStale) { + //drawable db is not stale, just open it + openTopComponent(); + } else { + //drawable db is stale, ask what to do + Alert alert = new Alert(Alert.AlertType.WARNING, + Bundle.OpenAction_stale_confDlg_msg(), + ButtonType.YES, ButtonType.NO, ButtonType.CANCEL); + alert.initModality(Modality.APPLICATION_MODAL); + alert.setTitle(Bundle.OpenAction_stale_confDlg_title()); + GuiUtils.setDialogIcons(alert); + ButtonType answer = alert.showAndWait().orElse(ButtonType.CANCEL); + if (answer == ButtonType.CANCEL) { + //just do nothing + } else if (answer == ButtonType.NO) { + openTopComponent(); + } else if (answer == ButtonType.YES) { + if (controller.getAutopsyCase().getCaseType() == Case.CaseType.SINGLE_USER_CASE) { + /* For a single-user case, we favor user + * experience, and rebuild the database as soon + * as Image Gallery is enabled for the case. + * + * Turning listening off is necessary in order + * to invoke the listener that will call + * controller.rebuildDB(); + */ + controller.setListeningEnabled(false); + controller.setListeningEnabled(true); + } else { + /* + * For a multi-user case, we favor overall + * performance and user experience, not every + * user may want to review images, so we rebuild + * the database only when a user launches Image + * Gallery. + */ + controller.rebuildDB(); + } + openTopComponent(); + } + } + }, + throwable -> logger.log(Level.SEVERE, "Error checking if drawable db is stale.", throwable)//NON-NLS + ); + } + + private void openTopComponent() { + SwingUtilities.invokeLater(() -> { + try { + ImageGalleryTopComponent.openTopComponent(); + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex);//NON-NLS + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS} + } + }); } @Override @@ -195,6 +260,6 @@ public final class OpenAction extends CallableSystemAction { @Override public boolean asynchronous() { - return false; // run on edt + return true; // run off edt } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java index 00d3c3d728..8b1f6eb31a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java @@ -99,7 +99,7 @@ public class GroupKey> implements Comparable if (!Objects.equals(this.attr, other.attr)) { return false; } - return Objects.equals(this.dataSource, other.dataSource); + return this.dataSource.getId() == other.dataSource.getId(); } @Override diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java index a9c4da2907..4884f580b7 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java @@ -82,7 +82,7 @@ public class SummaryTablePane extends AnchorPane { //register for category events controller.getCategoryManager().registerListener(this); - handleCategoryChanged(null); + new Thread(() -> handleCategoryChanged(null)).start(); } public SummaryTablePane(ImageGalleryController controller) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java index f1c4ac51bf..6ce152bfdb 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java @@ -74,6 +74,7 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupSortBy; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; +import static org.sleuthkit.autopsy.imagegallery.utils.TaskUtils.addFXCallback; import org.sleuthkit.datamodel.DataSource; /** @@ -255,6 +256,7 @@ public class Toolbar extends ToolBar { evt -> { Platform.runLater(() -> { Optional selectedItem = dataSourceSelectionModel.getSelectedItem(); + //restore selection once the sync is done. syncDataSources().addListener(() -> dataSourceSelectionModel.select(selectedItem), Platform::runLater); }); }); @@ -269,33 +271,30 @@ public class Toolbar extends ToolBar { } private void initTagMenuButton() { - ListenableFuture future = exec.submit(() -> new TagGroupAction(controller.getTagsManager().getFollowUpTagName(), controller)); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(TagGroupAction followUpGroupAction) { - tagGroupMenuButton.setOnAction(followUpGroupAction); - tagGroupMenuButton.setText(followUpGroupAction.getText()); - tagGroupMenuButton.setGraphic(followUpGroupAction.getGraphic()); - } - - @Override - public void onFailure(Throwable throwable) { - /* - * The problem appears to be a timing issue where a case is - * closed before this initialization is completed, which It - * appears to be harmless, so we are temporarily changing this - * log message to a WARNING. - * - * TODO (JIRA-3010): SEVERE error logged by image Gallery UI - */ - if (Case.isCaseOpen()) { - logger.log(Level.WARNING, "Could not create Follow Up tag menu item", throwable); //NON-NLS - } else { - // don't add stack trace to log because it makes looking for real errors harder - logger.log(Level.INFO, "Unable to get tag name. Case is closed."); //NON-NLS + addFXCallback(exec.submit(() -> new TagGroupAction(controller.getTagsManager().getFollowUpTagName(), controller)), + followUpGroupAction -> { + //on fx thread + tagGroupMenuButton.setOnAction(followUpGroupAction); + tagGroupMenuButton.setText(followUpGroupAction.getText()); + tagGroupMenuButton.setGraphic(followUpGroupAction.getGraphic()); + }, + throwable -> { + /* + * The problem appears to be a timing issue where a case is + * closed before this initialization is completed, which It + * appears to be harmless, so we are temporarily changing + * this log message to a WARNING. + * + * TODO (JIRA-3010): SEVERE error logged by image Gallery UI + */ + if (Case.isCaseOpen()) { + logger.log(Level.WARNING, "Could not create Follow Up tag menu item", throwable); //NON-NLS + } else { + // don't add stack trace to log because it makes looking for real errors harder + logger.log(Level.INFO, "Unable to get tag name. Case is closed."); //NON-NLS + } } - } - }, Platform::runLater); + ); tagGroupMenuButton.showingProperty().addListener(showing -> { if (tagGroupMenuButton.isShowing()) { @@ -303,24 +302,18 @@ public class Toolbar extends ToolBar { return Lists.transform(controller.getTagsManager().getNonCategoryTagNames(), tagName -> GuiUtils.createAutoAssigningMenuItem(tagGroupMenuButton, new TagGroupAction(tagName, controller))); }); - Futures.addCallback(getTagsFuture, new FutureCallback>() { - @Override - public void onSuccess(List result) { - tagGroupMenuButton.getItems().setAll(result); - } - @Override - public void onFailure(Throwable t) { - logger.log(Level.SEVERE, "Error getting non-gategory tag names.", t); - } - }, Platform::runLater); + addFXCallback(getTagsFuture, + menuItems -> tagGroupMenuButton.getItems().setAll(menuItems), + throwable -> logger.log(Level.SEVERE, "Error getting non-gategory tag names.", throwable) + ); } }); } @ThreadConfined(type = ThreadConfined.ThreadType.ANY) private ListenableFuture> syncDataSources() { - ListenableFuture> future = exec.submit(() -> { + ListenableFuture> dataSourcesFuture = exec.submit(() -> { List dataSourcesInCase = controller.getSleuthKitCase().getDataSources(); synchronized (dataSourcesViewable) { dataSourcesViewable.clear(); @@ -331,22 +324,18 @@ public class Toolbar extends ToolBar { } return dataSourcesInCase; }); - Futures.addCallback(future, new FutureCallback>() { - @Override - public void onSuccess(List result) { - List> newDataSources = new ArrayList<>(); - newDataSources.add(Optional.empty()); - result.forEach(dataSource -> newDataSources.add(Optional.of(dataSource))); - dataSources.setAll(newDataSources); - } + addFXCallback(dataSourcesFuture, + result -> { + //on fx thread + List> newDataSources = new ArrayList<>(Lists.transform(result, Optional::of)); + newDataSources.add(0, Optional.empty()); + dataSources.setAll(newDataSources); + }, + throwable -> logger.log(Level.SEVERE, "Unable to get datasources for current case.", throwable) //NON-NLS - @Override - public void onFailure(Throwable t) { - logger.log(Level.SEVERE, "Unable to get datasources for current case.", t); //NON-NLS - } - }, Platform::runLater); + ); - return future; + return dataSourcesFuture; } /** @@ -384,8 +373,7 @@ public class Toolbar extends ToolBar { * selection. */ private void syncGroupControlsEnabledState(GroupViewState newViewState) { - boolean noGroupSelected = (null == newViewState) - || (null == newViewState.getGroup()); + boolean noGroupSelected = (null == newViewState) || (null == newViewState.getGroup()); Platform.runLater(() -> { tagGroupMenuButton.setDisable(noGroupSelected); catGroupMenuButton.setDisable(noGroupSelected); @@ -402,7 +390,5 @@ public class Toolbar extends ToolBar { public Toolbar(ImageGalleryController controller) { this.controller = controller; FXMLConstructor.construct(this, "Toolbar.fxml"); //NON-NLS - } - } 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 990a19668a..26a7772759 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java @@ -21,12 +21,12 @@ package org.sleuthkit.autopsy.imagegallery.gui.drawableviews; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; +import static com.google.common.collect.Lists.transform; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.util.ArrayList; import java.util.Arrays; +import static java.util.Arrays.asList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -35,6 +35,7 @@ import java.util.Map; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.util.Optional; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.stream.IntStream; import javafx.animation.Interpolator; @@ -135,7 +136,10 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewMode; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils; +import static org.sleuthkit.autopsy.imagegallery.gui.GuiUtils.createAutoAssigningMenuItem; import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; +import static org.sleuthkit.autopsy.imagegallery.utils.TaskUtils.addFXCallback; +import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** @@ -254,7 +258,7 @@ public class GroupPane extends BorderPane { private final ReadOnlyObjectWrapper grouping = new ReadOnlyObjectWrapper<>(); /** - * map from fileIDs to their assigned cells in the tile view. This is used + * Map from fileIDs to their assigned cells in the tile view. This is used * to determine whether fileIDs are visible or are offscreen. No entry * indicates the given fileID is not displayed on screen. DrawableCells are * responsible for adding and removing themselves from this map. @@ -371,7 +375,7 @@ public class GroupPane extends BorderPane { case FIVE: return cat5Toggle; default: - throw new IllegalArgumentException(category.name()); + throw new UnsupportedOperationException("Unknown category: " + category.name()); } } @@ -425,51 +429,41 @@ public class GroupPane extends BorderPane { DoubleBinding cellSize = controller.thumbnailSizeProperty().add(75); gridView.cellHeightProperty().bind(cellSize); gridView.cellWidthProperty().bind(cellSize); - gridView.setCellFactory((GridView param) -> new DrawableCell()); + gridView.setCellFactory(param -> new DrawableCell()); BooleanBinding isSelectionEmpty = Bindings.isEmpty(selectionModel.getSelected()); catSelectedSplitMenu.disableProperty().bind(isSelectionEmpty); tagSelectedSplitMenu.disableProperty().bind(isSelectionEmpty); - TagSelectedFilesAction followUpSelectedAction = new TagSelectedFilesAction(controller.getTagsManager().getFollowUpTagName(), controller); //NON-NLS - Platform.runLater(() -> { - tagSelectedSplitMenu.setText(followUpSelectedAction.getText()); - tagSelectedSplitMenu.setGraphic(followUpSelectedAction.getGraphic()); - tagSelectedSplitMenu.setOnAction(followUpSelectedAction); - tagSelectedSplitMenu.showingProperty().addListener(showing -> { - if (tagSelectedSplitMenu.isShowing()) { - - ListenableFuture> getTagsFuture = exec.submit(() - -> Lists.transform(controller.getTagsManager().getNonCategoryTagNames(), - tagName -> GuiUtils.createAutoAssigningMenuItem(tagSelectedSplitMenu, new TagSelectedFilesAction(tagName, controller)))); - Futures.addCallback(getTagsFuture, new FutureCallback>() { - @Override - public void onSuccess(List result) { - tagSelectedSplitMenu.getItems().setAll(result); - } - - @Override - public void onFailure(Throwable throwable) { - logger.log(Level.SEVERE, "Error getting tag names.", throwable); - } - }, Platform::runLater); - } - }); - }); + addFXCallback(exec.submit(() -> controller.getTagsManager().getFollowUpTagName()), + followUpTagName -> { + //on fx thread + TagSelectedFilesAction followUpSelectedAction = new TagSelectedFilesAction(followUpTagName, controller); + tagSelectedSplitMenu.setText(followUpSelectedAction.getText()); + tagSelectedSplitMenu.setGraphic(followUpSelectedAction.getGraphic()); + tagSelectedSplitMenu.setOnAction(followUpSelectedAction); + }, + throwable -> logger.log(Level.SEVERE, "Error getting tag names.", throwable)); + addFXCallback(exec.submit(() -> controller.getTagsManager().getNonCategoryTagNames()), + tagNames -> { + //on fx thread + List menuItems = transform(tagNames, + tagName -> createAutoAssigningMenuItem(tagSelectedSplitMenu, new TagSelectedFilesAction(tagName, controller))); + tagSelectedSplitMenu.getItems().setAll(menuItems); + }, + throwable -> logger.log(Level.SEVERE, "Error getting tag names.", throwable)//NON-NLS + ); CategorizeSelectedFilesAction cat5SelectedAction = new CategorizeSelectedFilesAction(DhsImageCategory.FIVE, controller); catSelectedSplitMenu.setOnAction(cat5SelectedAction); catSelectedSplitMenu.setText(cat5SelectedAction.getText()); catSelectedSplitMenu.setGraphic(cat5SelectedAction.getGraphic()); - catSelectedSplitMenu.showingProperty().addListener(showing -> { - if (catSelectedSplitMenu.isShowing()) { - List categoryMenues = Lists.transform(Arrays.asList(DhsImageCategory.values()), - cat -> GuiUtils.createAutoAssigningMenuItem(catSelectedSplitMenu, new CategorizeSelectedFilesAction(cat, controller))); - catSelectedSplitMenu.getItems().setAll(categoryMenues); - } - }); + + List categoryMenues = transform(asList(DhsImageCategory.values()), + cat -> createAutoAssigningMenuItem(catSelectedSplitMenu, new CategorizeSelectedFilesAction(cat, controller))); + catSelectedSplitMenu.getItems().setAll(categoryMenues); slideShowToggle.getStyleClass().remove("radio-button"); slideShowToggle.getStyleClass().add("toggle-button"); @@ -681,7 +675,6 @@ public class GroupPane extends BorderPane { } void makeSelection(Boolean shiftDown, Long newFileID) { - if (shiftDown) { //TODO: do more hear to implement slicker multiselect int endIndex = grouping.get().getFileIDs().indexOf(newFileID); @@ -694,7 +687,6 @@ public class GroupPane extends BorderPane { } else { selectionAnchorIndex = null; selectionModel.clearAndSelect(newFileID); - } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java index 645c67baba..7766317995 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java @@ -22,6 +22,7 @@ import com.google.common.eventbus.Subscribe; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import static java.util.Collections.singletonMap; import java.util.List; import java.util.Objects; import static java.util.Objects.isNull; @@ -112,9 +113,7 @@ public class MetaDataPane extends DrawableUIBase { }); copyMenuItem.setAccelerator(COPY_KEY_COMBINATION); - copyMenuItem.setOnAction(actionEvent -> { - copyValueToClipBoard(); - }); + copyMenuItem.setOnAction(actionEvent -> copyValueToClipBoard()); tableView.setContextMenu(contextMenu); tableView.setOnKeyPressed((KeyEvent event) -> { @@ -220,9 +219,6 @@ public class MetaDataPane extends DrawableUIBase { return imageBorder; } - /** - * {@inheritDoc } - */ @Subscribe @Override public void handleCategoryChanged(CategoryManager.CategoryChangeEvent evt) { @@ -256,9 +252,9 @@ public class MetaDataPane extends DrawableUIBase { private void copyValueToClipBoard() { Pair, Collection> selectedItem = tableView.getSelectionModel().getSelectedItem(); if (nonNull(selectedItem)) { - Clipboard.getSystemClipboard().setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, - getValueDisplayString(selectedItem))); + Clipboard.getSystemClipboard().setContent( + singletonMap(DataFormat.PLAIN_TEXT, getValueDisplayString(selectedItem)) + ); } } - } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java index 11d109bbb5..e4d565d0ee 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java @@ -131,8 +131,8 @@ public class SlideShowView extends DrawableTileBase { getGroupPane().grouping().addListener(observable -> { syncButtonVisibility(); if (getGroupPane().getGroup() != null) { - getGroupPane().getGroup().getFileIDs().addListener((Observable observable1) -> - syncButtonVisibility()); + getGroupPane().getGroup().getFileIDs().addListener((Observable observable1) + -> syncButtonVisibility()); } }); } @@ -215,9 +215,7 @@ public class SlideShowView extends DrawableTileBase { mediaTask = null; } }); - myTask.setOnCancelled(cancelled -> { - disposeContent(); - }); + myTask.setOnCancelled(cancelled -> disposeContent()); exec.execute(myTask); return progressNode; @@ -245,7 +243,6 @@ public class SlideShowView extends DrawableTileBase { /** * - * @param file the value of file * @param imageTask the value of imageTask */ @Override @@ -259,9 +256,6 @@ public class SlideShowView extends DrawableTileBase { return maskerPane; } - /** - * {@inheritDoc } - */ @Override protected String getTextForLabel() { return getFile().map(DrawableFile::getName).orElse("") + " " + getSupplementalText(); @@ -292,8 +286,8 @@ public class SlideShowView extends DrawableTileBase { * of y" */ @NbBundle.Messages({"# {0} - file id number", - "# {1} - number of file ids", - "SlideShowView.supplementalText={0} of {1} in group"}) + "# {1} - number of file ids", + "SlideShowView.supplementalText={0} of {1} in group"}) private String getSupplementalText() { final ObservableList fileIds = getGroupPane().getGroup().getFileIDs(); return getFileID().map(fileID -> " ( " + Bundle.SlideShowView_supplementalText(fileIds.indexOf(fileID) + 1, fileIds.size()) + " )") @@ -301,9 +295,6 @@ public class SlideShowView extends DrawableTileBase { } - /** - * {@inheritDoc } - */ @Override @ThreadConfined(type = ThreadType.ANY) public DhsImageCategory updateCategory() { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java index 9c26b9788c..61fdcb4e1b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java @@ -18,11 +18,16 @@ */ package org.sleuthkit.autopsy.imagegallery.utils; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.util.concurrent.Callable; import java.util.concurrent.Executors; +import java.util.function.Consumer; +import javafx.application.Platform; import javafx.concurrent.Task; /** @@ -42,8 +47,26 @@ public final class TaskUtils { }; } - public static ListeningExecutorService getExecutorForClass(Class clazz) { + public static ListeningExecutorService getExecutorForClass(Class clazz) { return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor( new ThreadFactoryBuilder().setNameFormat("Image Gallery " + clazz.getSimpleName() + " BG Thread").build())); } + + public static void addFXCallback(ListenableFuture future, Consumer onSuccess, Consumer onFailure) { + Futures.addCallback(future, makeFutureCallBack(onSuccess, onFailure), Platform::runLater); + } + + public static FutureCallback< X> makeFutureCallBack(Consumer onSuccess, Consumer onFailure) { + return new FutureCallback() { + @Override + public void onSuccess(X result) { + onSuccess.accept(result); + } + + @Override + public void onFailure(Throwable t) { + onFailure.accept(t); + } + }; + } }