Merge branch 'develop' of https://github.com/sleuthkit/autopsy into 4167-UpdateCommonAttributeGUI

This commit is contained in:
William Schaefer 2018-09-17 10:30:26 -04:00
commit a2a0970e48
51 changed files with 2890 additions and 2112 deletions

View File

@ -329,6 +329,7 @@
<package>org.sleuthkit.autopsy.guiutils</package>
<package>org.sleuthkit.autopsy.healthmonitor</package>
<package>org.sleuthkit.autopsy.ingest</package>
<package>org.sleuthkit.autopsy.ingest.events</package>
<package>org.sleuthkit.autopsy.keywordsearchservice</package>
<package>org.sleuthkit.autopsy.menuactions</package>
<package>org.sleuthkit.autopsy.modules.encryptiondetection</package>

View File

@ -107,7 +107,7 @@ public class ImageUtils {
* NOTE: Must be cleared when the case is changed.
*/
@Messages({"ImageUtils.ffmpegLoadedError.title=OpenCV FFMpeg",
"ImageUtils.ffmpegLoadedError.msg=OpenCV FFMpeg library failed to load, see log for more details"})
"ImageUtils.ffmpegLoadedError.msg=OpenCV FFMpeg library failed to load, see log for more details"})
private static final ConcurrentHashMap<Long, File> cacheFileMap = new ConcurrentHashMap<>();
static {
@ -218,7 +218,7 @@ public class ImageUtils {
}
return VideoUtils.isVideoThumbnailSupported(file)
|| isImageThumbnailSupported(file);
|| isImageThumbnailSupported(file);
}
/**
@ -413,7 +413,7 @@ public class ImageUtils {
String cacheDirectory = Case.getCurrentCaseThrows().getCacheDirectory();
return Paths.get(cacheDirectory, "thumbnails", fileID + ".png").toFile(); //NON-NLS
} catch (NoCurrentCaseException e) {
LOGGER.log(Level.WARNING, "Could not get cached thumbnail location. No case is open."); //NON-NLS
LOGGER.log(Level.INFO, "Could not get cached thumbnail location. No case is open."); //NON-NLS
return null;
}
});

View File

@ -23,11 +23,13 @@ import org.openide.util.NbBundle;
import static org.sleuthkit.autopsy.directorytree.Bundle.*;
@NbBundle.Messages({"SelectionContext.dataSources=Data Sources",
"SelectionContext.dataSourceFiles=Data Source Files",
"SelectionContext.views=Views"})
enum SelectionContext {
DATA_SOURCES(SelectionContext_dataSources()),
VIEWS(SelectionContext_views()),
OTHER(""); // Subnode of another node.
OTHER(""), // Subnode of another node.
DATA_SOURCE_FILES(SelectionContext_dataSourceFiles());
private final String displayName;
@ -36,7 +38,7 @@ enum SelectionContext {
}
public static SelectionContext getContextFromName(String name) {
if (name.equals(DATA_SOURCES.getName())) {
if (name.equals(DATA_SOURCES.getName()) || name.equals(DATA_SOURCE_FILES.getName())) {
return DATA_SOURCES;
} else if (name.equals(VIEWS.getName())) {
return VIEWS;
@ -64,6 +66,16 @@ enum SelectionContext {
// One level below root node. Should be one of DataSources, Views, or Results
return SelectionContext.getContextFromName(n.getDisplayName());
} else {
// In Group by Data Source mode, the node under root is the data source name, and
// under that is Data Source Files, Views, or Results. Before moving up the tree, check
// if one of those applies.
if (n.getParentNode().getParentNode().getParentNode() == null) {
SelectionContext context = SelectionContext.getContextFromName(n.getDisplayName());
if (context != SelectionContext.OTHER) {
return context;
}
}
return getSelectionContext(n.getParentNode());
}
}

View File

@ -26,7 +26,7 @@ import org.sleuthkit.datamodel.Content;
* Event published when analysis (ingest) of a data source included in an ingest
* job is completed.
*/
public class DataSourceAnalysisCompletedEvent extends DataSourceAnalysisEvent implements Serializable {
public final class DataSourceAnalysisCompletedEvent extends DataSourceAnalysisEvent implements Serializable {
/**
* The reason why the analysis of the data source completed.

View File

@ -26,7 +26,7 @@ import org.sleuthkit.datamodel.Content;
* Event published when analysis (ingest) of a data source included in an ingest
* job is started.
*/
public class DataSourceAnalysisStartedEvent extends DataSourceAnalysisEvent implements Serializable {
public final class DataSourceAnalysisStartedEvent extends DataSourceAnalysisEvent implements Serializable {
private static final long serialVersionUID = 1L;

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015-17 Basis Technology Corp.
* Copyright 2015-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -56,9 +56,7 @@ public final class PromptDialogManager {
@NbBundle.Messages("PrompDialogManager.buttonType.update=Update DB")
private static final ButtonType UPDATE = new ButtonType(Bundle.PrompDialogManager_buttonType_update(), ButtonBar.ButtonData.OK_DONE);
/**
* Image to use as title bar icon in dialogs
*/
/** Image to use as title bar icon in dialogs */
private static final Image AUTOPSY_ICON;
static {
@ -222,7 +220,7 @@ public final class PromptDialogManager {
dialog.setHeaderText(Bundle.PromptDialogManager_showTooManyFiles_headerText());
dialog.showAndWait();
}
@NbBundle.Messages({
"PromptDialogManager.showTimeLineDisabledMessage.contentText="
+ "Timeline functionality is not available yet."

View File

@ -230,6 +230,7 @@
<package>org.apache.commons.codec.digest</package>
<package>org.apache.commons.codec.language</package>
<package>org.apache.commons.codec.net</package>
<package>org.apache.commons.collections4</package>
<package>org.apache.commons.csv</package>
<package>org.apache.commons.io</package>
<package>org.apache.commons.io.comparator</package>

View File

@ -21,21 +21,24 @@ package org.sleuthkit.autopsy.imagegallery;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyLongWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
@ -43,25 +46,14 @@ import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javax.annotation.Nullable;
import javax.swing.SwingUtilities;
import javax.annotation.Nonnull;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.util.Cancellable;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.Case.CaseType;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.core.RuntimeProperties;
import org.sleuthkit.autopsy.coreutils.History;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
@ -69,18 +61,18 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.actions.UndoRedoManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB.DrawableDbBuildStatusEnum;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.HashSetManager;
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.ingest.IngestManager;
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
@ -90,8 +82,7 @@ import org.sleuthkit.datamodel.TskData;
*/
public final class ImageGalleryController {
private static final Logger LOGGER = Logger.getLogger(ImageGalleryController.class.getName());
private static ImageGalleryController instance;
private static final Logger logger = Logger.getLogger(ImageGalleryController.class.getName());
/**
* true if Image Gallery should listen to ingest events, false if it should
@ -103,7 +94,7 @@ public final class ImageGalleryController {
private final ReadOnlyBooleanWrapper stale = new ReadOnlyBooleanWrapper(false);
private final ReadOnlyBooleanWrapper metaDataCollapsed = new ReadOnlyBooleanWrapper(false);
private final ReadOnlyDoubleWrapper thumbnailSize = new ReadOnlyDoubleWrapper(100);
private final SimpleDoubleProperty thumbnailSizeProp = new SimpleDoubleProperty(100);
private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false);
private final ReadOnlyIntegerWrapper dbTaskQueueSize = new ReadOnlyIntegerWrapper(0);
@ -111,36 +102,23 @@ public final class ImageGalleryController {
private final History<GroupViewState> historyManager = new History<>();
private final UndoRedoManager undoManager = new UndoRedoManager();
private final GroupManager groupManager = new GroupManager(this);
private final HashSetManager hashSetManager = new HashSetManager();
private final CategoryManager categoryManager = new CategoryManager(this);
private final DrawableTagsManager tagsManager = new DrawableTagsManager(null);
private Runnable showTree;
private Toolbar toolbar;
private StackPane fullUIStackPane;
private StackPane centralStackPane;
private Node infoOverlay;
private final Region infoOverLayBackground = new Region() {
{
setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
setOpacity(.4);
}
};
private final ThumbnailCache thumbnailCache = new ThumbnailCache(this);
private final GroupManager groupManager;
private final HashSetManager hashSetManager;
private final CategoryManager categoryManager;
private final DrawableTagsManager tagsManager;
private ListeningExecutorService dbExecutor;
private SleuthkitCase sleuthKitCase;
private DrawableDB db;
private final Case autopsyCase;
private final SleuthkitCase sleuthKitCase;
private final DrawableDB drawableDB;
public static synchronized ImageGalleryController getDefault() {
if (instance == null) {
instance = new ImageGalleryController();
}
return instance;
public Case getAutopsyCase() {
return autopsyCase;
}
public ReadOnlyBooleanProperty getMetaDataCollapsed() {
public ReadOnlyBooleanProperty metaDataCollapsedProperty() {
return metaDataCollapsed.getReadOnlyProperty();
}
@ -148,19 +126,19 @@ public final class ImageGalleryController {
this.metaDataCollapsed.set(metaDataCollapsed);
}
public ReadOnlyDoubleProperty thumbnailSizeProperty() {
return thumbnailSize.getReadOnlyProperty();
public DoubleProperty thumbnailSizeProperty() {
return thumbnailSizeProp;
}
private GroupViewState getViewState() {
public GroupViewState getViewState() {
return historyManager.getCurrentState();
}
public ReadOnlyBooleanProperty regroupDisabled() {
public ReadOnlyBooleanProperty regroupDisabledProperty() {
return regroupDisabled.getReadOnlyProperty();
}
public ReadOnlyObjectProperty<GroupViewState> viewState() {
public ReadOnlyObjectProperty<GroupViewState> viewStateProperty() {
return historyManager.currentState();
}
@ -172,8 +150,8 @@ public final class ImageGalleryController {
return groupManager;
}
synchronized public DrawableDB getDatabase() {
return db;
public DrawableDB getDatabase() {
return drawableDB;
}
public void setListeningEnabled(boolean enabled) {
@ -193,14 +171,9 @@ public final class ImageGalleryController {
Platform.runLater(() -> {
stale.set(b);
});
try {
new PerCaseProperties(Case.getCurrentCaseThrows()).setConfigSetting(ImageGalleryModule.getModuleName(), PerCaseProperties.STALE, b.toString());
} catch (NoCurrentCaseException ex) {
Logger.getLogger(ImageGalleryController.class.getName()).log(Level.WARNING, "Exception while getting open case."); //NON-NLS
}
}
public ReadOnlyBooleanProperty stale() {
public ReadOnlyBooleanProperty staleProperty() {
return stale.getReadOnlyProperty();
}
@ -209,50 +182,57 @@ public final class ImageGalleryController {
return stale.get();
}
private ImageGalleryController() {
ImageGalleryController(@Nonnull Case newCase) throws TskCoreException {
listeningEnabled.addListener((observable, oldValue, newValue) -> {
this.autopsyCase = Objects.requireNonNull(newCase);
this.sleuthKitCase = newCase.getSleuthkitCase();
setListeningEnabled(ImageGalleryModule.isEnabledforCase(newCase));
groupManager = new GroupManager(this);
this.drawableDB = DrawableDB.getDrawableDB(this);
categoryManager = new CategoryManager(this);
tagsManager = new DrawableTagsManager(this);
tagsManager.registerListener(groupManager);
tagsManager.registerListener(categoryManager);
hashSetManager = new HashSetManager(drawableDB);
setStale(isDataSourcesTableStale());
dbExecutor = getNewDBExecutor();
// listener for the boolean property about when IG is listening / enabled
listeningEnabled.addListener((observable, wasPreviouslyEnabled, isEnabled) -> {
try {
//if we just turned on listening and a case is open and that case is not up to date
if (newValue && !oldValue && ImageGalleryModule.isDrawableDBStale(Case.getCurrentCaseThrows())) {
// if we just turned on listening and a single-user case is open and that case is not up to date, then rebuild it
// For multiuser cases, we defer DB rebuild till the user actually opens Image Gallery
if (isEnabled && !wasPreviouslyEnabled
&& isDataSourcesTableStale()
&& (Case.getCurrentCaseThrows().getCaseType() == CaseType.SINGLE_USER_CASE)) {
//populate the db
queueDBTask(new CopyAnalyzedFiles(instance, db, sleuthKitCase));
this.rebuildDB();
}
} catch (NoCurrentCaseException ex) {
LOGGER.log(Level.WARNING, "Exception while getting open case.", ex);
logger.log(Level.WARNING, "Exception while getting open case.", ex);
}
});
groupManager.getAnalyzedGroups().addListener((Observable o) -> {
//analyzed groups is confined to JFX thread
if (Case.isCaseOpen()) {
checkForGroups();
}
});
groupManager.getUnSeenGroups().addListener((Observable observable) -> {
//if there are unseen groups and none being viewed
if (groupManager.getUnSeenGroups().isEmpty() == false && (getViewState() == null || getViewState().getGroup() == null)) {
advance(GroupViewState.tile(groupManager.getUnSeenGroups().get(0)), true);
}
});
viewState().addListener((Observable observable) -> {
viewStateProperty().addListener((Observable observable) -> {
//when the viewed group changes, clear the selection and the undo/redo history
selectionModel.clearSelection();
undoManager.clear();
});
regroupDisabled.addListener(observable -> checkForGroups());
IngestManager ingestManager = IngestManager.getInstance();
PropertyChangeListener ingestEventHandler =
propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled);
PropertyChangeListener ingestEventHandler
= propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled);
ingestManager.addIngestModuleEventListener(ingestEventHandler);
ingestManager.addIngestJobEventListener(ingestEventHandler);
dbTaskQueueSize.addListener(obs -> this.updateRegroupDisabled());
}
public ReadOnlyBooleanProperty getCanAdvance() {
@ -264,10 +244,7 @@ public final class ImageGalleryController {
}
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
public void advance(GroupViewState newState, boolean forceShowTree) {
if (forceShowTree && showTree != null) {
showTree.run();
}
public void advance(GroupViewState newState) {
historyManager.advance(newState);
}
@ -284,131 +261,92 @@ public final class ImageGalleryController {
regroupDisabled.set((dbTaskQueueSize.get() > 0) || IngestManager.getInstance().isIngestRunning());
}
/**
* Check if there are any fully analyzed groups available from the
* GroupManager and remove blocking progress spinners if there are. If there
* aren't, add a blocking progress spinner with appropriate message.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@NbBundle.Messages({"ImageGalleryController.noGroupsDlg.msg1=No groups are fully analyzed; but listening to ingest is disabled. "
+ " No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg2=No groups are fully analyzed yet, but ingest is still ongoing. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg3=No groups are fully analyzed yet, but image / video data is still being populated. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg4=There are no images/videos available from the added datasources; but listening to ingest is disabled. "
+ " No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg5=There are no images/videos in the added datasources.",
"ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:"
+ " the current Group By setting resulted in no groups, "
+ "or no groups are fully analyzed but ingest is not running."})
synchronized private void checkForGroups() {
if (groupManager.getAnalyzedGroups().isEmpty()) {
if (IngestManager.getInstance().isIngestRunning()) {
if (listeningEnabled.not().get()) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg1()));
} else {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg2(),
new ProgressIndicator()));
}
} else if (dbTaskQueueSize.get() > 0) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(),
new ProgressIndicator()));
} else if (db != null && db.countAllFiles() <= 0) { // there are no files in db
if (listeningEnabled.not().get()) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg4()));
} else {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg5()));
}
} else if (!groupManager.isRegrouping()) {
replaceNotification(centralStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg6()));
}
} else {
clearNotification();
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void clearNotification() {
//remove the ingest spinner
if (fullUIStackPane != null) {
fullUIStackPane.getChildren().remove(infoOverlay);
}
//remove the ingest spinner
if (centralStackPane != null) {
centralStackPane.getChildren().remove(infoOverlay);
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void replaceNotification(StackPane stackPane, Node newNode) {
clearNotification();
infoOverlay = new StackPane(infoOverLayBackground, newNode);
if (stackPane != null) {
stackPane.getChildren().add(infoOverlay);
}
}
/**
* configure the controller for a specific case.
*
* @param theNewCase the case to configure the controller for
*
* @throws org.sleuthkit.datamodel.TskCoreException
*/
public synchronized void setCase(Case theNewCase) {
if (null == theNewCase) {
reset();
} else {
this.sleuthKitCase = theNewCase.getSleuthkitCase();
this.db = DrawableDB.getDrawableDB(ImageGalleryModule.getModuleOutputDir(theNewCase), this);
setListeningEnabled(ImageGalleryModule.isEnabledforCase(theNewCase));
setStale(ImageGalleryModule.isDrawableDBStale(theNewCase));
// if we add this line icons are made as files are analyzed rather than on demand.
// db.addUpdatedFileListener(IconCache.getDefault());
historyManager.clear();
groupManager.setDB(db);
hashSetManager.setDb(db);
categoryManager.setDb(db);
tagsManager.setAutopsyTagsManager(theNewCase.getServices().getTagsManager());
tagsManager.registerListener(groupManager);
tagsManager.registerListener(categoryManager);
shutDownDBExecutor();
dbExecutor = getNewDBExecutor();
}
/**
* Rebuilds the DrawableDB database.
*
*/
public void rebuildDB() {
// queue a rebuild task for each stale data source
getStaleDataSourceIds().forEach(dataSourceObjId -> queueDBTask(new CopyAnalyzedFiles(dataSourceObjId, this)));
}
/**
* reset the state of the controller (eg if the case is closed)
*/
public synchronized void reset() {
LOGGER.info("resetting ImageGalleryControler to initial state."); //NON-NLS
logger.info("Closing ImageGalleryControler for case."); //NON-NLS
selectionModel.clearSelection();
setListeningEnabled(false);
ThumbnailCache.getDefault().clearCache();
thumbnailCache.clearCache();
historyManager.clear();
groupManager.clear();
tagsManager.clearFollowUpTagName();
tagsManager.unregisterListener(groupManager);
tagsManager.unregisterListener(categoryManager);
groupManager.reset();
shutDownDBExecutor();
dbExecutor = getNewDBExecutor();
}
if (toolbar != null) {
toolbar.reset();
/**
* Checks if the datasources table in drawable DB is stale.
*
* @return true if datasources table is stale
*/
public boolean isDataSourcesTableStale() {
return isNotEmpty(getStaleDataSourceIds());
}
/**
* Returns a set of data source object ids that are stale.
*
* This includes any data sources already in the table, that are not in
* COMPLETE status, or any data sources that might have been added to the
* case, but are not in the datasources table.
*
* @return list of data source object ids that are stale.
*/
Set<Long> getStaleDataSourceIds() {
Set<Long> staleDataSourceIds = new HashSet<>();
// no current case open to check
if ((null == getDatabase()) || (null == getSleuthKitCase())) {
return staleDataSourceIds;
}
if (db != null) {
db.closeDBCon();
try {
Map<Long, DrawableDbBuildStatusEnum> knownDataSourceIds = getDatabase().getDataSourceDbBuildStatus();
List<DataSource> dataSources = getSleuthKitCase().getDataSources();
Set<Long> caseDataSourceIds = new HashSet<>();
dataSources.stream().map(DataSource::getId).forEach(caseDataSourceIds::add);
// collect all data sources already in the table, that are not yet COMPLETE
knownDataSourceIds.entrySet().stream().forEach((Map.Entry<Long, DrawableDbBuildStatusEnum> t) -> {
DrawableDbBuildStatusEnum status = t.getValue();
if (DrawableDbBuildStatusEnum.COMPLETE != status) {
staleDataSourceIds.add(t.getKey());
}
});
// collect any new data sources in the case.
caseDataSourceIds.forEach((Long id) -> {
if (!knownDataSourceIds.containsKey(id)) {
staleDataSourceIds.add(id);
}
});
return staleDataSourceIds;
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Image Gallery failed to check if datasources table is stale.", ex);
return staleDataSourceIds;
}
db = null;
}
synchronized private void shutDownDBExecutor() {
@ -417,7 +355,7 @@ public final class ImageGalleryController {
try {
dbExecutor.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
LOGGER.log(Level.WARNING, "Image Gallery failed to shutdown DB Task Executor in a timely fashion.", ex);
logger.log(Level.WARNING, "Image Gallery failed to shutdown DB Task Executor in a timely fashion.", ex);
}
}
}
@ -449,46 +387,14 @@ public final class ImageGalleryController {
Platform.runLater(() -> dbTaskQueueSize.set(dbTaskQueueSize.get() - 1));
}
@Nullable
synchronized public DrawableFile getFileFromId(Long fileID) throws TskCoreException {
if (Objects.isNull(db)) {
LOGGER.log(Level.WARNING, "Could not get file from id, no DB set. The case is probably closed."); //NON-NLS
return null;
}
return db.getFileFromID(fileID);
}
public void setStacks(StackPane fullUIStack, StackPane centralStack) {
fullUIStackPane = fullUIStack;
this.centralStackPane = centralStack;
Platform.runLater(this::checkForGroups);
}
public synchronized void setToolbar(Toolbar toolbar) {
if (this.toolbar != null) {
throw new IllegalStateException("Can not set the toolbar a second time!");
}
this.toolbar = toolbar;
thumbnailSize.bind(toolbar.thumbnailSizeProperty());
public DrawableFile getFileFromID(Long fileID) throws TskCoreException {
return drawableDB.getFileFromID(fileID);
}
public ReadOnlyDoubleProperty regroupProgress() {
return groupManager.regroupProgress();
}
/**
* invoked by {@link OnStart} to make sure that the ImageGallery listeners
* get setup as early as possible, and do other setup stuff.
*/
void onStart() {
Platform.setImplicitExit(false);
LOGGER.info("setting up ImageGallery listeners"); //NON-NLS
//TODO can we do anything usefull in an InjestJobEventListener?
//IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {});
IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener());
Case.addPropertyChangeListener(new CaseEventListener());
}
public HashSetManager getHashSetManager() {
return hashSetManager;
}
@ -501,10 +407,6 @@ public final class ImageGalleryController {
return tagsManager;
}
public void setShowTree(Runnable showTree) {
this.showTree = showTree;
}
public UndoRedoManager getUndoManager() {
return undoManager;
}
@ -515,6 +417,12 @@ public final class ImageGalleryController {
public synchronized SleuthkitCase getSleuthKitCase() {
return sleuthKitCase;
}
public ThumbnailCache getThumbsCache() {
return thumbnailCache;
}
/**
@ -594,7 +502,7 @@ public final class ImageGalleryController {
return file;
}
public FileTask(AbstractFile f, DrawableDB taskDB) {
FileTask(AbstractFile f, DrawableDB taskDB) {
super();
this.file = f;
this.taskDB = taskDB;
@ -604,7 +512,7 @@ public final class ImageGalleryController {
/**
* task that updates one file in database with results from ingest
*/
static private class UpdateFileTask extends FileTask {
static class UpdateFileTask extends FileTask {
UpdateFileTask(AbstractFile f, DrawableDB taskDB) {
super(f, taskDB);
@ -631,7 +539,7 @@ public final class ImageGalleryController {
/**
* task that updates one file in database with results from ingest
*/
static private class RemoveFileTask extends FileTask {
static class RemoveFileTask extends FileTask {
RemoveFileTask(AbstractFile f, DrawableDB taskDB) {
super(f, taskDB);
@ -654,51 +562,74 @@ public final class ImageGalleryController {
}
}
/**
* Base abstract class for various methods of copying image files data, for
* a given data source, into the Image gallery DB.
*/
@NbBundle.Messages({"BulkTask.committingDb.status=committing image/video database",
"BulkTask.stopCopy.status=Stopping copy to drawable db task.",
"BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."})
/* Base abstract class for various methods of copying data into the Image gallery DB */
abstract static private class BulkTransferTask extends BackgroundTask {
abstract static class BulkTransferTask extends BackgroundTask {
static private final String FILE_EXTENSION_CLAUSE =
"(name LIKE '%." //NON-NLS
+ String.join("' OR name LIKE '%.", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS
+ "')";
static private final String FILE_EXTENSION_CLAUSE
= "(extension LIKE '" //NON-NLS
+ String.join("' OR extension LIKE '", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS
+ "') ";
static private final String MIMETYPE_CLAUSE =
"(mime_type LIKE '" //NON-NLS
+ String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS
+ "') ";
static private final String MIMETYPE_CLAUSE
= "(mime_type LIKE '" //NON-NLS
+ String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS
+ "') ";
static final String DRAWABLE_QUERY =
//grab files with supported extension
"(" + FILE_EXTENSION_CLAUSE
//grab files with supported mime-types
+ " OR " + MIMETYPE_CLAUSE //NON-NLS
//grab files with image or video mime-types even if we don't officially support them
+ " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )"; //NON-NLS
private final String DRAWABLE_QUERY;
private final String DATASOURCE_CLAUSE;
final ImageGalleryController controller;
final DrawableDB taskDB;
final SleuthkitCase tskCase;
protected final ImageGalleryController controller;
protected final DrawableDB taskDB;
protected final SleuthkitCase tskCase;
protected final long dataSourceObjId;
ProgressHandle progressHandle;
private ProgressHandle progressHandle;
private boolean taskCompletionStatus;
BulkTransferTask(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
BulkTransferTask(long dataSourceObjId, ImageGalleryController controller) {
this.controller = controller;
this.taskDB = taskDB;
this.tskCase = tskCase;
this.taskDB = controller.getDatabase();
this.tskCase = controller.getSleuthKitCase();
this.dataSourceObjId = dataSourceObjId;
DATASOURCE_CLAUSE = " (data_source_obj_id = " + dataSourceObjId + ") ";
DRAWABLE_QUERY
= DATASOURCE_CLAUSE
+ " AND ( "
+ //grab files with supported extension
FILE_EXTENSION_CLAUSE
//grab files with supported mime-types
+ " OR " + MIMETYPE_CLAUSE //NON-NLS
//grab files with image or video mime-types even if we don't officially support them
+ " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )"; //NON-NLS
}
/**
*
* Do any cleanup for this task.
*
* @param success true if the transfer was successful
*/
abstract void cleanup(boolean success);
abstract List<AbstractFile> getFiles() throws TskCoreException;
abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDBTransaction) throws TskCoreException;
abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) throws TskCoreException;
/**
* Gets a list of files to process.
*
* @return list of files to process
*
* @throws TskCoreException
*/
List<AbstractFile> getFiles() throws TskCoreException {
return tskCase.findAllFilesWhere(DRAWABLE_QUERY);
}
@Override
public void run() {
@ -706,24 +637,32 @@ public final class ImageGalleryController {
progressHandle.start();
updateMessage(Bundle.CopyAnalyzedFiles_populatingDb_status());
DrawableDB.DrawableTransaction drawableDbTransaction = null;
CaseDbTransaction caseDbTransaction = null;
try {
//grab all files with supported extension or detected mime types
final List<AbstractFile> files = getFiles();
progressHandle.switchToDeterminate(files.size());
taskDB.insertOrUpdateDataSource(dataSourceObjId, DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS);
updateProgress(0.0);
taskCompletionStatus = true;
int workDone = 0;
//do in transaction
DrawableDB.DrawableTransaction tr = taskDB.beginTransaction();
int workDone = 0;
drawableDbTransaction = taskDB.beginTransaction();
caseDbTransaction = tskCase.beginTransaction();
for (final AbstractFile f : files) {
if (isCancelled() || Thread.interrupted()) {
LOGGER.log(Level.WARNING, "Task cancelled: not all contents may be transfered to drawable database."); //NON-NLS
logger.log(Level.WARNING, "Task cancelled or interrupted: not all contents may be transfered to drawable database."); //NON-NLS
taskCompletionStatus = false;
progressHandle.finish();
break;
}
processFile(f, tr);
processFile(f, drawableDbTransaction, caseDbTransaction);
workDone++;
progressHandle.progress(f.getName(), workDone);
@ -737,23 +676,42 @@ public final class ImageGalleryController {
updateProgress(1.0);
progressHandle.start();
taskDB.commitTransaction(tr, true);
caseDbTransaction.commit();
taskDB.commitTransaction(drawableDbTransaction, true);
} catch (TskCoreException ex) {
if (null != drawableDbTransaction) {
taskDB.rollbackTransaction(drawableDbTransaction);
}
if (null != caseDbTransaction) {
try {
caseDbTransaction.rollback();
} catch (TskCoreException ex2) {
logger.log(Level.SEVERE, "Error in trying to rollback transaction", ex2); //NON-NLS
}
}
progressHandle.progress(Bundle.BulkTask_stopCopy_status());
LOGGER.log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents", ex); //NON-NLS
logger.log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents", ex); //NON-NLS
MessageNotifyUtil.Notify.warn(Bundle.BulkTask_errPopulating_errMsg(), ex.getMessage());
cleanup(false);
return;
} finally {
progressHandle.finish();
if (taskCompletionStatus) {
taskDB.insertOrUpdateDataSource(dataSourceObjId, DrawableDB.DrawableDbBuildStatusEnum.COMPLETE);
}
updateMessage("");
updateProgress(-1.0);
}
cleanup(true);
cleanup(taskCompletionStatus);
}
abstract ProgressHandle getInitialProgressHandle();
protected void setTaskCompletionStatus(boolean status) {
taskCompletionStatus = status;
}
}
/**
@ -766,24 +724,21 @@ public final class ImageGalleryController {
@NbBundle.Messages({"CopyAnalyzedFiles.committingDb.status=committing image/video database",
"CopyAnalyzedFiles.stopCopy.status=Stopping copy to drawable db task.",
"CopyAnalyzedFiles.errPopulating.errMsg=There was an error populating Image Gallery database."})
static private class CopyAnalyzedFiles extends BulkTransferTask {
static class CopyAnalyzedFiles extends BulkTransferTask {
CopyAnalyzedFiles(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
super(controller, taskDB, tskCase);
CopyAnalyzedFiles(long dataSourceObjId, ImageGalleryController controller) {
super(dataSourceObjId, controller);
}
@Override
protected void cleanup(boolean success) {
controller.setStale(!success);
// at the end of the task, set the stale status based on the
// cumulative status of all data sources
controller.setStale(controller.isDataSourcesTableStale());
}
@Override
List<AbstractFile> getFiles() throws TskCoreException {
return tskCase.findAllFilesWhere(DRAWABLE_QUERY);
}
@Override
void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr) {
void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDbTransaction) throws TskCoreException {
final boolean known = f.getKnown() == TskData.FileKnown.KNOWN;
if (known) {
@ -791,13 +746,21 @@ public final class ImageGalleryController {
} else {
try {
if (FileTypeUtils.hasDrawableMIMEType(f)) { //supported mimetype => analyzed
taskDB.updateFile(DrawableFile.create(f, true, false), tr);
} else { //unsupported mimtype => analyzed but shouldn't include
taskDB.removeFile(f.getId(), tr);
//supported mimetype => analyzed
if (null != f.getMIMEType() && FileTypeUtils.hasDrawableMIMEType(f)) {
taskDB.updateFile(DrawableFile.create(f, true, false), tr, caseDbTransaction);
} else {
// if mimetype of the file hasn't been ascertained, ingest might not have completed yet.
if (null == f.getMIMEType()) {
// set to false to force the DB to be marked as stale
this.setTaskCompletionStatus(false);
} else {
//unsupported mimtype => analyzed but shouldn't include
taskDB.removeFile(f.getId(), tr);
}
}
} catch (FileTypeDetector.FileTypeDetectorInitException ex) {
throw new RuntimeException(ex);
throw new TskCoreException("Failed to initialize FileTypeDetector.", ex);
}
}
}
@ -813,24 +776,17 @@ public final class ImageGalleryController {
* Copy files from a newly added data source into the DB. Get all "drawable"
* files, based on extension and mime-type. After ingest we use file type id
* module and if necessary jpeg/png signature matching to add/remove files
*
* TODO: create methods to simplify progress value/text updates to both
* netbeans and ImageGallery progress/status
*/
@NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=committing image/video database"})
static private class PrePopulateDataSourceFiles extends BulkTransferTask {
private static final Logger LOGGER = Logger.getLogger(PrePopulateDataSourceFiles.class.getName());
private final Content dataSource;
static class PrePopulateDataSourceFiles extends BulkTransferTask {
/**
*
* @param dataSourceId Data source object ID
* @param dataSourceObjId The object ID of the DataSource that is being
* pre-populated into the DrawableDB.
* @param controller The controller for this task.
*/
PrePopulateDataSourceFiles(Content dataSource, ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
super(controller, taskDB, tskCase);
this.dataSource = dataSource;
PrePopulateDataSourceFiles(long dataSourceObjId, ImageGalleryController controller) {
super(dataSourceObjId, controller);
}
@Override
@ -838,14 +794,8 @@ public final class ImageGalleryController {
}
@Override
void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) {
taskDB.insertFile(DrawableFile.create(f, false, false), tr);
}
@Override
List<AbstractFile> getFiles() throws TskCoreException {
long datasourceID = dataSource.getDataSource().getId();
return tskCase.findAllFilesWhere("data_source_obj_id = " + datasourceID + " AND " + DRAWABLE_QUERY);
void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDBTransaction) {
taskDB.insertFile(DrawableFile.create(f, false, false), tr, caseDBTransaction);
}
@Override
@ -854,116 +804,4 @@ public final class ImageGalleryController {
return ProgressHandle.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this);
}
}
private class IngestModuleEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.runningWithGUI() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
IngestManager.getInstance().removeIngestModuleEventListener(this);
return;
}
switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) {
case CONTENT_CHANGED:
//TODO: do we need to do anything here? -jm
case DATA_ADDED:
/*
* we could listen to DATA events and progressivly update
* files, and get data from DataSource ingest modules, but
* given that most modules don't post new artifacts in the
* events and we would have to query for them, without
* knowing which are the new ones, we just ignore these
* events for now. The relevant data should all be captured
* by file done event, anyways -jm
*/
break;
case FILE_DONE:
/**
* getOldValue has fileID getNewValue has
* {@link Abstractfile}
*/
AbstractFile file = (AbstractFile) evt.getNewValue();
if (isListeningEnabled()) {
if (file.isFile()) {
try {
synchronized (ImageGalleryController.this) {
if (ImageGalleryModule.isDrawableAndNotKnown(file)) {
//this file should be included and we don't already know about it from hash sets (NSRL)
queueDBTask(new UpdateFileTask(file, db));
} else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) {
//doing this check results in fewer tasks queued up, and faster completion of db update
//this file would have gotten scooped up in initial grab, but actually we don't need it
queueDBTask(new RemoveFileTask(file, db));
}
}
} catch (TskCoreException | FileTypeDetector.FileTypeDetectorInitException ex) {
//TODO: What to do here?
LOGGER.log(Level.SEVERE, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS
MessageNotifyUtil.Notify.error("Image Gallery Error",
"Unable to determine if file is drawable and not known. Not making any changes to DB. See the logs for details.");
}
}
} else { //TODO: keep track of what we missed for later
setStale(true);
}
break;
}
}
}
private class CaseEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.runningWithGUI() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
Case.removePropertyChangeListener(this);
return;
}
switch (Case.Events.valueOf(evt.getPropertyName())) {
case CURRENT_CASE:
Case newCase = (Case) evt.getNewValue();
if (newCase == null) { // case is closing
//close window, reset everything
SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent);
reset();
} else { // a new case has been opened
setCase(newCase); //connect db, groupmanager, start worker thread
}
break;
case DATA_SOURCE_ADDED:
//copy all file data to drawable databse
Content newDataSource = (Content) evt.getNewValue();
if (isListeningEnabled()) {
queueDBTask(new PrePopulateDataSourceFiles(newDataSource, ImageGalleryController.this, getDatabase(), getSleuthKitCase()));
} else {//TODO: keep track of what we missed for later
setStale(true);
}
break;
case CONTENT_TAG_ADDED:
final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt;
if (getDatabase().isInDB(tagAddedEvent.getAddedTag().getContent().getId())) {
getTagsManager().fireTagAddedEvent(tagAddedEvent);
}
break;
case CONTENT_TAG_DELETED:
final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) evt;
if (getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) {
getTagsManager().fireTagDeletedEvent(tagDeletedEvent);
}
break;
}
}
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-15 Basis Technology Corp.
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -18,32 +18,77 @@
*/
package org.sleuthkit.autopsy.imagegallery;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.commons.lang3.StringUtils;
import java.util.logging.Level;
import javafx.application.Platform;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.core.RuntimeProperties;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.events.AutopsyEvent;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent;
import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.FILE_DONE;
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
/** static definitions and utilities for the ImageGallery module */
/** static definitions, utilities, and listeners for the ImageGallery module */
@NbBundle.Messages({"ImageGalleryModule.moduleName=Image Gallery"})
public class ImageGalleryModule {
private static final Logger LOGGER = Logger.getLogger(ImageGalleryModule.class.getName());
private static final Logger logger = Logger.getLogger(ImageGalleryModule.class.getName());
private static final String MODULE_NAME = Bundle.ImageGalleryModule_moduleName();
private static final Object controllerLock = new Object();
private static ImageGalleryController controller;
public static ImageGalleryController getController() throws NoCurrentCaseException {
synchronized (controllerLock) {
if (controller == null) {
try {
controller = new ImageGalleryController(Case.getCurrentCaseThrows());
} catch (NoCurrentCaseException | TskCoreException ex) {
throw new NoCurrentCaseException("Error getting ImageGalleryController for the current case.", ex);
}
}
return controller;
}
}
/**
*
*
* This method is invoked by virtue of the OnStart annotation on the OnStart
* class class
*/
static void onStart() {
Platform.setImplicitExit(false);
logger.info("Setting up ImageGallery listeners"); //NON-NLS
IngestManager.getInstance().addIngestJobEventListener(new IngestJobEventListener());
IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener());
Case.addPropertyChangeListener(new CaseEventListener());
}
static String getModuleName() {
return MODULE_NAME;
}
/**
* get the Path to the Case's ImageGallery ModuleOutput subfolder; ie
* ".../[CaseName]/ModuleOutput/Image Gallery/"
@ -53,7 +98,7 @@ public class ImageGalleryModule {
*
* @return the Path to the ModuleOuput subfolder for Image Gallery
*/
static Path getModuleOutputDir(Case theCase) {
public static Path getModuleOutputDir(Case theCase) {
return Paths.get(theCase.getModuleDirectory(), getModuleName());
}
@ -83,19 +128,10 @@ public class ImageGalleryModule {
* @return true if the drawable db is out of date for the given case, false
* otherwise
*/
public static boolean isDrawableDBStale(Case c) {
if (c != null) {
String stale = new PerCaseProperties(c).getConfigSetting(ImageGalleryModule.MODULE_NAME, PerCaseProperties.STALE);
return StringUtils.isNotBlank(stale) ? Boolean.valueOf(stale) : true;
} else {
return false;
}
public static boolean isDrawableDBStale(Case c) throws TskCoreException {
return new ImageGalleryController(c).isDataSourcesTableStale();
}
/**
* Is the given file 'supported' and not 'known'(nsrl hash hit). If so we
* should include it in {@link DrawableDB} and UI
@ -105,7 +141,187 @@ public class ImageGalleryModule {
* @return true if the given {@link AbstractFile} is "drawable" and not
* 'known', else false
*/
public static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws TskCoreException, FileTypeDetector.FileTypeDetectorInitException {
public static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws FileTypeDetector.FileTypeDetectorInitException {
return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && FileTypeUtils.isDrawable(abstractFile);
}
/**
* Listener for IngestModuleEvents
*/
static private class IngestModuleEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.runningWithGUI() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
IngestManager.getInstance().removeIngestModuleEventListener(this);
return;
}
if (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName()) != FILE_DONE) {
return;
}
// getOldValue has fileID getNewValue has Abstractfile
AbstractFile file = (AbstractFile) evt.getNewValue();
if (false == file.isFile()) {
return;
}
/* only process individual files in realtime on the node that is
* running the ingest. on a remote node, image files are processed
* enblock when ingest is complete */
if (((AutopsyEvent) evt).getSourceType() != AutopsyEvent.SourceType.LOCAL) {
return;
}
try {
ImageGalleryController con = getController();
if (con.isListeningEnabled()) {
try {
if (isDrawableAndNotKnown(file)) {
//this file should be included and we don't already know about it from hash sets (NSRL)
con.queueDBTask(new ImageGalleryController.UpdateFileTask(file, controller.getDatabase()));
} else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) {
/* Doing this check results in fewer tasks queued
* up, and faster completion of db update. This file
* would have gotten scooped up in initial grab, but
* actually we don't need it */
con.queueDBTask(new ImageGalleryController.RemoveFileTask(file, controller.getDatabase()));
}
} catch (FileTypeDetector.FileTypeDetectorInitException ex) {
logger.log(Level.SEVERE, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS
MessageNotifyUtil.Notify.error("Image Gallery Error",
"Unable to determine if file is drawable and not known. Not making any changes to DB. See the logs for details.");
}
}
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS
}
}
}
/**
* Listener for case events.
*/
static private class CaseEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.runningWithGUI() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
Case.removePropertyChangeListener(this);
return;
}
ImageGalleryController con;
try {
con = getController();
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS
return;
}
switch (Case.Events.valueOf(evt.getPropertyName())) {
case CURRENT_CASE:
synchronized (controllerLock) {
// case has changes: close window, reset everything
SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent);
if (controller != null) {
controller.reset();
}
controller = null;
Case newCase = (Case) evt.getNewValue();
if (newCase != null) {
// a new case has been opened: connect db, groupmanager, start worker thread
try {
controller = new ImageGalleryController(newCase);
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error changing case in ImageGallery.", ex);
}
}
}
break;
case DATA_SOURCE_ADDED:
//For a data source added on the local node, prepopulate all file data to drawable database
if (((AutopsyEvent) evt).getSourceType() == AutopsyEvent.SourceType.LOCAL) {
Content newDataSource = (Content) evt.getNewValue();
if (con.isListeningEnabled()) {
con.queueDBTask(new ImageGalleryController.PrePopulateDataSourceFiles(newDataSource.getId(), controller));
}
}
break;
case CONTENT_TAG_ADDED:
final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt;
if (con.getDatabase().isInDB(tagAddedEvent.getAddedTag().getContent().getId())) {
con.getTagsManager().fireTagAddedEvent(tagAddedEvent);
}
break;
case CONTENT_TAG_DELETED:
final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) evt;
if (con.getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) {
con.getTagsManager().fireTagDeletedEvent(tagDeletedEvent);
}
break;
default:
//we don't need to do anything for other events.
break;
}
}
}
/**
* Listener for Ingest Job events.
*/
static private class IngestJobEventListener implements PropertyChangeListener {
@NbBundle.Messages({
"ImageGalleryController.dataSourceAnalyzed.confDlg.msg= A new data source was added and finished ingest.\n"
+ "The image / video database may be out of date. "
+ "Do you want to update the database with ingest results?\n",
"ImageGalleryController.dataSourceAnalyzed.confDlg.title=Image Gallery"
})
@Override
public void propertyChange(PropertyChangeEvent evt) {
IngestJobEvent eventType = IngestJobEvent.valueOf(evt.getPropertyName());
if (eventType != IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED
|| ((AutopsyEvent) evt).getSourceType() != AutopsyEvent.SourceType.REMOTE) {
return;
}
// A remote node added a new data source and just finished ingest on it.
//drawable db is stale, and if ImageGallery is open, ask user what to do
ImageGalleryController con;
try {
con = getController();
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS
return;
}
con.setStale(true);
if (con.isListeningEnabled() && ImageGalleryTopComponent.isImageGalleryOpen()) {
SwingUtilities.invokeLater(() -> {
int showAnswer = JOptionPane.showConfirmDialog(ImageGalleryTopComponent.getTopComponent(),
Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_msg(),
Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_title(),
JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
switch (showAnswer) {
case JOptionPane.YES_OPTION:
con.rebuildDB();
break;
case JOptionPane.NO_OPTION:
case JOptionPane.CANCEL_OPTION:
default:
break; //do nothing
}
});
}
}
}
}

View File

@ -18,13 +18,10 @@
*/
package org.sleuthkit.autopsy.imagegallery;
import java.awt.event.ActionEvent;
import java.util.logging.Level;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.autopsy.coreutils.Logger;
/**
* The Image/Video Gallery panel in the NetBeans provided Options Dialogs
@ -45,13 +42,8 @@ final class ImageGalleryOptionsPanel extends javax.swing.JPanel {
enabledForCaseBox.setEnabled(Case.isCaseOpen() && IngestManager.getInstance().isIngestRunning() == false);
});
enabledByDefaultBox.addActionListener((ActionEvent e) -> {
controller.changed();
});
enabledForCaseBox.addActionListener((ActionEvent e) -> {
controller.changed();
});
enabledByDefaultBox.addActionListener(actionEvent -> controller.changed());
enabledForCaseBox.addActionListener(actionEvent -> controller.changed());
}
/**
@ -204,19 +196,18 @@ final class ImageGalleryOptionsPanel extends javax.swing.JPanel {
void store() {
ImageGalleryPreferences.setEnabledByDefault(enabledByDefaultBox.isSelected());
ImageGalleryController.getDefault().setListeningEnabled(enabledForCaseBox.isSelected());
ImageGalleryPreferences.setGroupCategorizationWarningDisabled(groupCategorizationWarningBox.isSelected());
// If a case is open, save the per case setting
try {
Case openCase = Case.getCurrentCaseThrows();
ImageGalleryModule.getController().setListeningEnabled(enabledForCaseBox.isSelected());
new PerCaseProperties(openCase).setConfigSetting(ImageGalleryModule.getModuleName(), PerCaseProperties.ENABLED, Boolean.toString(enabledForCaseBox.isSelected()));
} catch (NoCurrentCaseException ex) {
// It's not an error if there's no case open
}
}
/**

View File

@ -46,8 +46,7 @@ public class ImageGalleryPreferences {
* @return true if new cases should have image analyzer enabled.
*/
public static boolean isEnabledByDefault() {
final boolean aBoolean = preferences.getBoolean(ENABLED_BY_DEFAULT, true);
return aBoolean;
return preferences.getBoolean(ENABLED_BY_DEFAULT, true);
}
public static void setEnabledByDefault(boolean b) {

View File

@ -18,27 +18,52 @@
*/
package org.sleuthkit.autopsy.imagegallery;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.embed.swing.JFXPanel;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceDialog;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TabPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Modality;
import javax.swing.SwingUtilities;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import static org.apache.commons.lang3.ObjectUtils.notEqual;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.ExplorerUtils;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
import org.openide.windows.Mode;
import org.openide.windows.RetainLocation;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils;
import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog;
import org.sleuthkit.autopsy.imagegallery.gui.StatusBar;
import org.sleuthkit.autopsy.imagegallery.gui.SummaryTablePane;
import org.sleuthkit.autopsy.imagegallery.gui.Toolbar;
@ -46,6 +71,9 @@ import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.GroupPane;
import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.MetaDataPane;
import org.sleuthkit.autopsy.imagegallery.gui.navpanel.GroupTree;
import org.sleuthkit.autopsy.imagegallery.gui.navpanel.HashHitGroupList;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Top component which displays ImageGallery interface.
@ -69,17 +97,17 @@ import org.sleuthkit.autopsy.imagegallery.gui.navpanel.HashHitGroupList;
public final class ImageGalleryTopComponent extends TopComponent implements ExplorerManager.Provider, Lookup.Provider {
public final static String PREFERRED_ID = "ImageGalleryTopComponent"; // NON-NLS
private static final Logger LOGGER = Logger.getLogger(ImageGalleryTopComponent.class.getName());
private static final Logger logger = Logger.getLogger(ImageGalleryTopComponent.class.getName());
private static volatile boolean topComponentInitialized = false;
private final ExplorerManager em = new ExplorerManager();
private final Lookup lookup = (ExplorerUtils.createLookup(em, getActionMap()));
private final ImageGalleryController controller = ImageGalleryController.getDefault();
private ImageGalleryController controller;
private SplitPane splitPane;
private StackPane centralStack;
private BorderPane borderPane = new BorderPane();
private final BorderPane borderPane = new BorderPane();
private StackPane fullUIStack;
private MetaDataPane metaDataTable;
private GroupPane groupPane;
@ -88,24 +116,97 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
private VBox leftPane;
private Scene myScene;
public static void openTopComponent() {
//TODO:eventually move to this model, throwing away everything and rebuilding controller groupmanager etc for each case.
// synchronized (OpenTimelineAction.class) {
// if (timeLineController == null) {
// timeLineController = new TimeLineController();
// LOGGER.log(Level.WARNING, "Failed to get TimeLineController from lookup. Instantiating one directly.S");
// }
// }
// timeLineController.openTimeLine();
final TopComponent tc = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
if (tc != null) {
topComponentInitialized = true;
if (tc.isOpened() == false) {
tc.open();
}
tc.toFront();
tc.requestActive();
private Node infoOverlay;
private final Region infoOverLayBackground = new TranslucentRegion();
/**
* Returns whether the ImageGallery window is open or not.
*
* @return true, if Image gallery is opened, false otherwise
*/
public static boolean isImageGalleryOpen() {
final TopComponent topComponent = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
if (topComponent != null) {
return topComponent.isOpened();
}
return false;
}
/**
* Returns the top component window.
*
* @return Image gallery top component window, null if it's not open
*/
public static TopComponent getTopComponent() {
return WindowManager.getDefault().findTopComponent(PREFERRED_ID);
}
@Messages({
"ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.headerText=Choose a data source to view.",
"ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.contentText=Data source:",
"ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.all=All",
"ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.titleText=Image Gallery",})
public static void openTopComponent() throws NoCurrentCaseException {
final TopComponent topComponent = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
if (topComponent == null) {
return;
}
topComponentInitialized = true;
if (topComponent.isOpened()) {
showTopComponent(topComponent);
return;
}
List<DataSource> dataSources = Collections.emptyList();
ImageGalleryController controller = ImageGalleryModule.getController();
((ImageGalleryTopComponent) topComponent).setController(controller);
try {
dataSources = controller.getSleuthKitCase().getDataSources();
} catch (TskCoreException tskCoreException) {
logger.log(Level.SEVERE, "Unable to get data sourcecs.", tskCoreException);
}
GroupManager groupManager = controller.getGroupManager();
synchronized (groupManager) {
if (dataSources.size() <= 1
|| groupManager.getGroupBy() != DrawableAttribute.PATH) {
/* if there is only one datasource or the grouping is already
* set to something other than path , don't both to ask for
* datasource */
groupManager.regroup(null, groupManager.getGroupBy(), groupManager.getSortBy(), groupManager.getSortOrder(), true);
showTopComponent(topComponent);
return;
}
}
Map<String, DataSource> dataSourceNames = new HashMap<>();
dataSourceNames.put("All", null);
dataSources.forEach(dataSource -> dataSourceNames.put(dataSource.getName(), dataSource));
Platform.runLater(() -> {
ChoiceDialog<String> datasourceDialog = new ChoiceDialog<>(null, dataSourceNames.keySet());
datasourceDialog.setTitle(Bundle.ImageGalleryTopComponent_openTopCommponent_chooseDataSourceDialog_titleText());
datasourceDialog.setHeaderText(Bundle.ImageGalleryTopComponent_openTopCommponent_chooseDataSourceDialog_headerText());
datasourceDialog.setContentText(Bundle.ImageGalleryTopComponent_openTopCommponent_chooseDataSourceDialog_contentText());
datasourceDialog.initModality(Modality.APPLICATION_MODAL);
GuiUtils.setDialogIcons(datasourceDialog);
Optional<String> dataSourceName = datasourceDialog.showAndWait();
DataSource dataSource = dataSourceName.map(dataSourceNames::get).orElse(null);
synchronized (groupManager) {
groupManager.regroup(dataSource, groupManager.getGroupBy(), groupManager.getSortBy(), groupManager.getSortOrder(), true);
}
SwingUtilities.invokeLater(() -> showTopComponent(topComponent));
});
}
@ThreadConfined(type = ThreadConfined.ThreadType.AWT)
public static void showTopComponent(TopComponent topComponent) {
topComponent.open();
topComponent.toFront();
topComponent.requestActive();
}
public static void closeTopComponent() {
@ -115,48 +216,57 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
try {
etc.close();
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "failed to close " + PREFERRED_ID, e); // NON-NLS
logger.log(Level.SEVERE, "failed to close " + PREFERRED_ID, e); // NON-NLS
}
}
}
}
public ImageGalleryTopComponent() {
public ImageGalleryTopComponent() throws NoCurrentCaseException {
setName(Bundle.CTL_ImageGalleryTopComponent());
initComponents();
setController(ImageGalleryModule.getController());
}
Platform.runLater(() -> {//initialize jfx ui
fullUIStack = new StackPane(); //this is passed into controller
myScene = new Scene(fullUIStack);
jfxPanel.setScene(myScene);
groupPane = new GroupPane(controller);
centralStack = new StackPane(groupPane); //this is passed into controller
fullUIStack.getChildren().add(borderPane);
splitPane = new SplitPane();
borderPane.setCenter(splitPane);
Toolbar toolbar = new Toolbar(controller);
borderPane.setTop(toolbar);
borderPane.setBottom(new StatusBar(controller));
synchronized private void setController(ImageGalleryController controller) {
if (this.controller != null && notEqual(this.controller, controller)) {
this.controller.reset();
}
this.controller = controller;
Platform.runLater(new Runnable() {
@Override
public void run() {
//initialize jfx ui
fullUIStack = new StackPane(); //this is passed into controller
myScene = new Scene(fullUIStack);
jfxPanel.setScene(myScene);
groupPane = new GroupPane(controller);
centralStack = new StackPane(groupPane); //this is passed into controller
fullUIStack.getChildren().add(borderPane);
splitPane = new SplitPane();
borderPane.setCenter(splitPane);
Toolbar toolbar = new Toolbar(controller);
borderPane.setTop(toolbar);
borderPane.setBottom(new StatusBar(controller));
metaDataTable = new MetaDataPane(controller);
groupTree = new GroupTree(controller);
hashHitList = new HashHitGroupList(controller);
TabPane tabPane = new TabPane(groupTree, hashHitList);
tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE);
tabPane.setMinWidth(TabPane.USE_PREF_SIZE);
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);
splitPane.getItems().addAll(leftPane, centralStack, metaDataTable);
splitPane.setDividerPositions(0.1, 1.0);
metaDataTable = new MetaDataPane(controller);
controller.regroupDisabledProperty().addListener((Observable observable) -> checkForGroups());
controller.getGroupManager().getAnalyzedGroups().addListener((Observable observable) -> Platform.runLater(() -> checkForGroups()));
groupTree = new GroupTree(controller);
hashHitList = new HashHitGroupList(controller);
TabPane tabPane = new TabPane(groupTree, hashHitList);
tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE);
tabPane.setMinWidth(TabPane.USE_PREF_SIZE);
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);
splitPane.getItems().addAll(leftPane, centralStack, metaDataTable);
splitPane.setDividerPositions(0.1, 1.0);
ImageGalleryController.getDefault().setStacks(fullUIStack, centralStack);
ImageGalleryController.getDefault().setToolbar(toolbar);
ImageGalleryController.getDefault().setShowTree(() -> tabPane.getSelectionModel().select(groupTree));
Platform.runLater(() -> checkForGroups());
}
});
}
@ -212,4 +322,100 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
public Lookup getLookup() {
return lookup;
}
/**
* Check if there are any fully analyzed groups available from the
* GroupManager and remove blocking progress spinners if there are. If there
* aren't, add a blocking progress spinner with appropriate message.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@NbBundle.Messages({
"ImageGalleryController.noGroupsDlg.msg1=No groups are fully analyzed; but listening to ingest is disabled. "
+ " No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg2=No groups are fully analyzed yet, but ingest is still ongoing. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg3=No groups are fully analyzed yet, but image / video data is still being populated. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg4=There are no images/videos available from the added datasources; but listening to ingest is disabled. "
+ " No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg5=There are no images/videos in the added datasources.",
"ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:"
+ " the current Group By setting resulted in no groups, "
+ "or no groups are fully analyzed but ingest is not running."})
private void checkForGroups() {
GroupManager groupManager = controller.getGroupManager();
synchronized (groupManager) {
if (isNotEmpty(groupManager.getAnalyzedGroups())) {
clearNotification();
return;
}
if (IngestManager.getInstance().isIngestRunning()) {
if (controller.isListeningEnabled()) {
replaceNotification(centralStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg2(),
new ProgressIndicator()));
} else {
replaceNotification(fullUIStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg1()));
}
return;
}
if (controller.getDBTasksQueueSizeProperty().get() > 0) {
replaceNotification(fullUIStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(),
new ProgressIndicator()));
return;
}
try {
if (controller.getDatabase().countAllFiles() <= 0) {
// there are no files in db
if (controller.isListeningEnabled()) {
replaceNotification(fullUIStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg5()));
} else {
replaceNotification(fullUIStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg4()));
}
return;
}
} catch (TskCoreException tskCoreException) {
logger.log(Level.SEVERE, "Error counting files in the database.", tskCoreException);
}
if (false == groupManager.isRegrouping()) {
replaceNotification(centralStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg6()));
}
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void replaceNotification(StackPane stackPane, Node newNode) {
clearNotification();
infoOverlay = new StackPane(infoOverLayBackground, newNode);
if (stackPane != null) {
stackPane.getChildren().add(infoOverlay);
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void clearNotification() {
//remove the ingest spinner
fullUIStack.getChildren().remove(infoOverlay);
//remove the ingest spinner
centralStack.getChildren().remove(infoOverlay);
}
/**
* Region with partialy opacity used to block out parts of the UI behind a
* pseudo dialog.
*/
static final private class TranslucentRegion extends Region {
TranslucentRegion() {
setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
setOpacity(.4);
}
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013 Basis Technology Corp.
* Copyright 2013-2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -18,26 +18,19 @@
*/
package org.sleuthkit.autopsy.imagegallery;
import org.sleuthkit.autopsy.coreutils.Logger;
/**
*
* The {@link org.openide.modules.OnStart} annotation tells NetBeans to invoke
* this class's {@link OnStart#run()} method
* The org.openide.modules.OnStart annotation tells NetBeans to invoke this
* class's run method.
*/
@org.openide.modules.OnStart
public class OnStart implements Runnable {
static private final Logger LOGGER = Logger.getLogger(OnStart.class.getName());
/**
*
*
* This method is invoked by virtue of the {@link OnStart} annotation on the
* {@link ImageGalleryModule} class
* This method is invoked by virtue of the OnStart annotation on the this
* class
*/
@Override
public void run() {
ImageGalleryController.getDefault().onStart();
ImageGalleryModule.onStart();
}
}

View File

@ -40,8 +40,6 @@ class PerCaseProperties {
public static final String ENABLED = "enabled"; //NON-NLS
public static final String STALE = "stale"; //NON-NLS
private final Case theCase;
PerCaseProperties(Case c) {

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-15 Basis Technology Corp.
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -54,9 +54,13 @@ import org.sleuthkit.datamodel.TskCoreException;
* TODO: this was only a singleton for convenience, convert this to
* non-singleton class -jm?
*/
public enum ThumbnailCache {
public class ThumbnailCache {
instance;
private final ImageGalleryController controller;
public ThumbnailCache(ImageGalleryController controller) {
this.controller = controller;
}
private static final int MAX_THUMBNAIL_SIZE = 300;
@ -71,10 +75,6 @@ public enum ThumbnailCache {
.softValues()
.expireAfterAccess(10, TimeUnit.MINUTES).build();
public static ThumbnailCache getDefault() {
return instance;
}
/**
* currently desired icon size. is bound in {@link Toolbar}
*/
@ -109,7 +109,7 @@ public enum ThumbnailCache {
@Nullable
public Image get(Long fileID) {
try {
return get(ImageGalleryController.getDefault().getFileFromId(fileID));
return get(controller.getFileFromID(fileID));
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "Failed to load thumbnail for file: " + fileID, ex.getCause()); //NON-NLS
return null;

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-2017 Basis Technology Corp.
* Copyright 2013-2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -36,7 +36,6 @@ import javax.swing.SwingWorker;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils;
import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog;
@ -50,8 +49,8 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskData;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
/**
* Instances of this Action allow users to apply tags to content.
@ -75,14 +74,14 @@ public class AddTagAction extends Action {
setEventHandler(actionEvent -> addTagWithComment(""));
}
static public Menu getTagMenu(ImageGalleryController controller) {
static public Menu getTagMenu(ImageGalleryController controller) throws TskCoreException {
return new TagMenu(controller);
}
private void addTagWithComment(String comment) {
addTagsToFiles(tagName, comment, selectedFileIDs);
}
@NbBundle.Messages({"# {0} - fileID",
"AddDrawableTagAction.addTagsToFiles.alert=Unable to tag file {0}."})
private void addTagsToFiles(TagName tagName, String comment, Set<Long> selectedFiles) {
@ -94,7 +93,7 @@ public class AddTagAction extends Action {
DrawableTagsManager tagsManager = controller.getTagsManager();
for (Long fileID : selectedFiles) {
try {
final DrawableFile file = controller.getFileFromId(fileID);
final DrawableFile file = controller.getFileFromID(fileID);
LOGGER.log(Level.INFO, "tagging {0} with {1} and comment {2}", new Object[]{file.getName(), tagName.getDisplayName(), comment}); //NON-NLS
List<ContentTag> contentTags = tagsManager.getContentTags(file);
@ -141,7 +140,7 @@ public class AddTagAction extends Action {
"AddDrawableTagAction.displayName.singular=Tag File"})
private static class TagMenu extends Menu {
TagMenu(ImageGalleryController controller) {
TagMenu(ImageGalleryController controller) throws TskCoreException {
setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon()));
ObservableSet<Long> selectedFileIDs = controller.getSelectionModel().getSelected();
setText(selectedFileIDs.size() > 1
@ -163,11 +162,10 @@ public class AddTagAction extends Action {
empty.setDisable(true);
quickTagMenu.getItems().add(empty);
} else {
for (final TagName tagName : tagNames) {
AddTagAction addDrawableTagAction = new AddTagAction(controller, tagName, selectedFileIDs);
MenuItem tagNameItem = ActionUtils.createMenuItem(addDrawableTagAction);
quickTagMenu.getItems().add(tagNameItem);
}
tagNames.stream()
.map(tagName -> new AddTagAction(controller, tagName, selectedFileIDs))
.map(ActionUtils::createMenuItem)
.forEachOrdered(quickTagMenu.getItems()::add);
}
/*

View File

@ -138,7 +138,7 @@ public class CategorizeAction extends Action {
TagName catZeroTagName = categoryManager.getTagName(DhsImageCategory.ZERO);
for (long fileID : fileIDs) {
try {
DrawableFile file = controller.getFileFromId(fileID); //drawable db access
DrawableFile file = controller.getFileFromID(fileID); //drawable db access
if (createUndo) {
DhsImageCategory oldCat = file.getCategory(); //drawable db access
TagName oldCatTagName = categoryManager.getTagName(oldCat);

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015-16 Basis Technology Corp.
* Copyright 2015-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -34,11 +34,12 @@ import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import static org.apache.commons.lang.ObjectUtils.notEqual;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryPreferences;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.datamodel.TskCoreException;
/**
@ -52,32 +53,35 @@ public class CategorizeGroupAction extends CategorizeAction {
public CategorizeGroupAction(DhsImageCategory newCat, ImageGalleryController controller) {
super(controller, newCat, null);
setEventHandler(actionEvent -> {
ObservableList<Long> fileIDs = controller.viewState().get().getGroup().getFileIDs();
controller.getViewState().getGroup().ifPresent(group -> {
ObservableList<Long> fileIDs = group.getFileIDs();
if (ImageGalleryPreferences.isGroupCategorizationWarningDisabled()) {
//if they have preveiously disabled the warning, just go ahead and apply categories.
addCatToFiles(ImmutableSet.copyOf(fileIDs));
} else {
final Map<DhsImageCategory, Long> catCountMap = new HashMap<>();
for (Long fileID : fileIDs) {
try {
DhsImageCategory category = controller.getFileFromId(fileID).getCategory();
if (false == DhsImageCategory.ZERO.equals(category) && newCat.equals(category) == false) {
catCountMap.merge(category, 1L, Long::sum);
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to categorize files.", ex);
}
}
if (catCountMap.isEmpty()) {
//if there are not going to be any categories overwritten, skip the warning.
if (ImageGalleryPreferences.isGroupCategorizationWarningDisabled()) {
//if they have preveiously disabled the warning, just go ahead and apply categories.
addCatToFiles(ImmutableSet.copyOf(fileIDs));
} else {
showConfirmationDialog(catCountMap, newCat, fileIDs);
final Map<DhsImageCategory, Long> catCountMap = new HashMap<>();
for (Long fileID : fileIDs) {
try {
DhsImageCategory category = controller.getFileFromID(fileID).getCategory();
if (false == DhsImageCategory.ZERO.equals(category) && newCat.equals(category) == false) {
catCountMap.merge(category, 1L, Long::sum);
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to categorize files.", ex);
}
}
if (catCountMap.isEmpty()) {
//if there are not going to be any categories overwritten, skip the warning.
addCatToFiles(ImmutableSet.copyOf(fileIDs));
} else {
showConfirmationDialog(catCountMap, newCat, fileIDs);
}
}
}
});
});
}
@ -88,19 +92,18 @@ public class CategorizeGroupAction extends CategorizeAction {
"CategorizeGroupAction.fileCountHeader=Files in the following categories will have their categories overwritten: "})
private void showConfirmationDialog(final Map<DhsImageCategory, Long> catCountMap, DhsImageCategory newCat, ObservableList<Long> fileIDs) {
ButtonType categorizeButtonType =
new ButtonType(Bundle.CategorizeGroupAction_OverwriteButton_text(), ButtonBar.ButtonData.APPLY);
ButtonType categorizeButtonType
= new ButtonType(Bundle.CategorizeGroupAction_OverwriteButton_text(), ButtonBar.ButtonData.APPLY);
VBox textFlow = new VBox();
for (Map.Entry<DhsImageCategory, Long> entry : catCountMap.entrySet()) {
if (entry.getKey().equals(newCat) == false) {
if (entry.getValue() > 0) {
Label label = new Label(Bundle.CategorizeGroupAction_fileCountMessage(entry.getValue(), entry.getKey().getDisplayName()),
entry.getKey().getGraphic());
label.setContentDisplay(ContentDisplay.RIGHT);
textFlow.getChildren().add(label);
}
if (entry.getValue() > 0
&& notEqual(entry.getKey(), newCat)) {
Label label = new Label(Bundle.CategorizeGroupAction_fileCountMessage(entry.getValue(), entry.getKey().getDisplayName()),
entry.getKey().getGraphic());
label.setContentDisplay(ContentDisplay.RIGHT);
textFlow.getChildren().add(label);
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2011-17 Basis Technology Corp.
* Copyright 2011-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -18,15 +18,16 @@
*/
package org.sleuthkit.autopsy.imagegallery.actions;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.Optional;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.ObjectExpression;
import javafx.collections.ObservableList;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import org.apache.commons.collections4.CollectionUtils;
import org.controlsfx.control.action.Action;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
@ -36,58 +37,87 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
* Marks the currently displayed group as "seen" and advances to the next unseen
* group
*/
@NbBundle.Messages({"NextUnseenGroup.markGroupSeen=Mark Group Seen",
"NextUnseenGroup.nextUnseenGroup=Next Unseen group"})
@NbBundle.Messages({
"NextUnseenGroup.markGroupSeen=Mark Group Seen",
"NextUnseenGroup.nextUnseenGroup=Next Unseen group",
"NextUnseenGroup.allGroupsSeen=All Groups Have Been Seen"})
public class NextUnseenGroup extends Action {
private static final Image END =
new Image(NextUnseenGroup.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/control-stop.png")); //NON-NLS
private static final Image ADVANCE =
new Image(NextUnseenGroup.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/control-double.png")); //NON-NLS
private static final String IMAGE_PATH = "/org/sleuthkit/autopsy/imagegallery/images/"; //NON-NLS
private static final Image END_IMAGE = new Image(NextUnseenGroup.class.getResourceAsStream(
IMAGE_PATH + "control-stop.png")); //NON-NLS
private static final Image ADVANCE_IMAGE = new Image(NextUnseenGroup.class.getResourceAsStream(
IMAGE_PATH + "control-double.png")); //NON-NLS
private static final String MARK_GROUP_SEEN = Bundle.NextUnseenGroup_markGroupSeen();
private static final String NEXT_UNSEEN_GROUP = Bundle.NextUnseenGroup_nextUnseenGroup();
private static final String ALL_GROUPS_SEEN = Bundle.NextUnseenGroup_allGroupsSeen();
private final GroupManager groupManager;
private final ImageGalleryController controller;
private final ObservableList<DrawableGroup> unSeenGroups;
private final ObservableList<DrawableGroup> analyzedGroups;
private final GroupManager groupManager;
public NextUnseenGroup(ImageGalleryController controller) {
super(NEXT_UNSEEN_GROUP);
setGraphic(new ImageView(ADVANCE_IMAGE));
this.controller = controller;
groupManager = controller.getGroupManager();
unSeenGroups = groupManager.getUnSeenGroups();
analyzedGroups = groupManager.getAnalyzedGroups();
setGraphic(new ImageView(ADVANCE));
unSeenGroups.addListener((Observable observable) -> updateButton());
controller.viewStateProperty().addListener((Observable observable) -> updateButton());
//TODO: do we need both these listeners?
analyzedGroups.addListener((Observable o) -> this.updateButton());
unSeenGroups.addListener((Observable o) -> this.updateButton());
setEventHandler(event -> {
//fx-thread
setEventHandler(event -> { //on fx-thread
//if there is a group assigned to the view, mark it as seen
Optional.ofNullable(controller.viewState())
.map(ObjectExpression<GroupViewState>::getValue)
.map(GroupViewState::getGroup)
.ifPresent(group -> groupManager.markGroupSeen(group, true));
if (unSeenGroups.isEmpty() == false) {
controller.advance(GroupViewState.tile(unSeenGroups.get(0)), true);
updateButton();
}
Optional.ofNullable(controller.getViewState())
.flatMap(GroupViewState::getGroup)
.ifPresent(group -> {
groupManager.markGroupSeen(group, true)
.addListener(this::advanceToNextUnseenGroup, MoreExecutors.newDirectExecutorService());
});
});
updateButton();
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void advanceToNextUnseenGroup() {
synchronized (groupManager) {
if (CollectionUtils.isNotEmpty(unSeenGroups)) {
controller.advance(GroupViewState.tile(unSeenGroups.get(0)));
}
updateButton();
}
}
private void updateButton() {
setDisabled(unSeenGroups.isEmpty());
if (unSeenGroups.size() <= 1) {
setText(MARK_GROUP_SEEN);
setGraphic(new ImageView(END));
int size = unSeenGroups.size();
if (size < 1) {
//there are no unseen groups.
Platform.runLater(() -> {
setDisabled(true);
setText(ALL_GROUPS_SEEN);
setGraphic(null);
});
} else {
setText(NEXT_UNSEEN_GROUP);
setGraphic(new ImageView(ADVANCE));
DrawableGroup get = unSeenGroups.get(0);
DrawableGroup orElse = Optional.ofNullable(controller.getViewState()).flatMap(GroupViewState::getGroup).orElse(null);
boolean equals = get.equals(orElse);
if (size == 1 & equals) {
//The only unseen group is the one that is being viewed.
Platform.runLater(() -> {
setDisabled(false);
setText(MARK_GROUP_SEEN);
setGraphic(new ImageView(END_IMAGE));
});
} else {
//there are more unseen groups.
Platform.runLater(() -> {
setDisabled(false);
setText(NEXT_UNSEEN_GROUP);
setGraphic(new ImageView(ADVANCE_IMAGE));
});
}
}
}
}

View File

@ -21,16 +21,11 @@ package org.sleuthkit.autopsy.imagegallery.actions;
import java.awt.Component;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.net.URL;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JMenuItem;
@ -49,42 +44,27 @@ 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.ImageGalleryTopComponent;
import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils;
import org.sleuthkit.datamodel.TskCoreException;
@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.imagegallery.OpenAction")
@ActionReferences(value = {
@ActionReference(path = "Menu/Tools", position = 101),
@ActionReference(path = "Menu/Tools", position = 101)
,
@ActionReference(path = "Toolbars/Case", position = 101)})
@ActionRegistration(displayName = "#CTL_OpenAction", lazy = false)
@Messages({"CTL_OpenAction=Images/Videos",
"OpenAction.stale.confDlg.msg=The image / video database may be out of date. " +
"Do you want to update and listen for further ingest results?\n" +
"Choosing 'yes' will update the database and enable listening to future ingests.",
"OpenAction.stale.confDlg.msg=The image / video database may be out of date. "
+ "Do you want to update and listen for further ingest results?\n"
+ "Choosing 'yes' will update the database and enable listening to future ingests.",
"OpenAction.stale.confDlg.title=Image Gallery"})
public final class OpenAction extends CallableSystemAction {
private static final Logger logger = Logger.getLogger(OpenAction.class.getName());
private static final String VIEW_IMAGES_VIDEOS = Bundle.CTL_OpenAction();
/**
* Image to use as title bar icon in dialogs
*/
private static final Image AUTOPSY_ICON;
static {
Image tempImg = null;
try {
tempImg = new Image(new URL("nbresloc:/org/netbeans/core/startup/frame.gif").openStream()); //NON-NLS
} catch (IOException ex) {
logger.log(Level.WARNING, "Failed to load branded icon for progress dialog.", ex); //NON-NLS
}
AUTOPSY_ICON = tempImg;
}
private static final long FILE_LIMIT = 6_000_000;
private final PropertyChangeListener pcl;
@ -145,10 +125,7 @@ public final class OpenAction extends CallableSystemAction {
}
@Override
@SuppressWarnings("fallthrough")
@NbBundle.Messages({
"OpenAction.dialogTitle=Image Gallery"
})
@NbBundle.Messages({"OpenAction.dialogTitle=Image Gallery"})
public void performAction() {
//check case
@ -165,24 +142,49 @@ public final class OpenAction extends CallableSystemAction {
setEnabled(false);
return;
}
if (ImageGalleryModule.isDrawableDBStale(currentCase)) {
//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);
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:
ImageGalleryController.getDefault().setListeningEnabled(true);
//fall through
case JOptionPane.NO_OPTION:
ImageGalleryTopComponent.openTopComponent();
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) {
controller.setListeningEnabled(true);
} else {
controller.rebuildDB();
}
ImageGalleryTopComponent.openTopComponent();
break;
case JOptionPane.NO_OPTION: {
ImageGalleryTopComponent.openTopComponent();
}
break;
case JOptionPane.CANCEL_OPTION:
break; //do nothing
case JOptionPane.CANCEL_OPTION:
break; //do nothing
}
} else {
//drawable db is not stale, just open it
ImageGalleryTopComponent.openTopComponent();
}
} else {
//drawable db is not stale, just open it
ImageGalleryTopComponent.openTopComponent();
} catch (NoCurrentCaseException noCurrentCaseException) {
logger.log(Level.WARNING, "There was no case open when Image Gallery was opened.", noCurrentCaseException);
}
}
@ -198,16 +200,6 @@ public final class OpenAction extends CallableSystemAction {
return false;
}
/**
* Set the title bar icon for the given Dialog to be the Autopsy logo icon.
*
* @param dialog The dialog to set the title bar icon for.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private static void setDialogIcons(Dialog<?> dialog) {
((Stage) dialog.getDialogPane().getScene().getWindow()).getIcons().setAll(AUTOPSY_ICON);
}
@NbBundle.Messages({
"ImageGallery.showTooManyFiles.contentText="
+ "There are too many files in the DB to ensure reasonable performance."
@ -218,7 +210,7 @@ public final class OpenAction extends CallableSystemAction {
Bundle.ImageGallery_showTooManyFiles_contentText(), ButtonType.OK);
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setTitle(Bundle.OpenAction_dialogTitle());
setDialogIcons(dialog);
GuiUtils.setDialogIcons(dialog);
dialog.setHeaderText(Bundle.ImageGallery_showTooManyFiles_headerText());
dialog.showAndWait();
}

View File

@ -29,9 +29,10 @@ public class TagGroupAction extends AddTagAction {
public TagGroupAction(final TagName tagName, ImageGalleryController controller) {
super(controller, tagName, null);
setEventHandler(actionEvent ->
new AddTagAction(controller, tagName, ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs())).
handle(actionEvent)
);
setEventHandler(actionEvent -> {
controller.getViewState().getGroup().ifPresent(group -> {
new AddTagAction(controller, tagName, ImmutableSet.copyOf(group.getFileIDs())).handle(actionEvent);
});
});
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015-16 Basis Technology Corp.
* Copyright 2015-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -34,20 +34,19 @@ import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Provides a cached view of the number of files per category, and fires
* {@link CategoryChangeEvent}s when files are categorized.
* CategoryChangeEvents when files are categorized.
*
* To receive CategoryChangeEvents, a listener must register itself, and
* implement a public method annotated with {@link Subscribe} that accepts one
* argument of type CategoryChangeEvent
* implement a public method annotated with Subscribe that accepts one argument
* of type CategoryChangeEvent
*
* TODO: currently these two functions (cached counts and events) are separate
* although they are related. Can they be integrated more?
@ -64,14 +63,14 @@ public class CategoryManager {
* initialized from this, and the counting of CAT-0 is always delegated to
* this db.
*/
private DrawableDB db;
private final DrawableDB drawableDb;
/**
* Used to distribute {@link CategoryChangeEvent}s
* Used to distribute CategoryChangeEvents
*/
private final EventBus categoryEventBus = new AsyncEventBus(Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder().namingPattern("Category Event Bus").uncaughtExceptionHandler((Thread t, Throwable e) -> { //NON-NLS
LOGGER.log(Level.SEVERE, "Uncaught exception in category event bus handler", e); //NON-NLS
new BasicThreadFactory.Builder().namingPattern("Category Event Bus").uncaughtExceptionHandler((Thread thread, Throwable throwable) -> { //NON-NLS
LOGGER.log(Level.SEVERE, "Uncaught exception in category event bus handler", throwable); //NON-NLS
}).build()
));
@ -80,38 +79,29 @@ public class CategoryManager {
* the count related methods go through this cache, which loads initial
* values from the database if needed.
*/
private final LoadingCache<DhsImageCategory, LongAdder> categoryCounts =
CacheBuilder.newBuilder().build(CacheLoader.from(this::getCategoryCountHelper));
private final LoadingCache<DhsImageCategory, LongAdder> categoryCounts
= CacheBuilder.newBuilder().build(CacheLoader.from(this::getCategoryCountHelper));
/**
* cached TagNames corresponding to Categories, looked up from
* autopsyTagManager at initial request or if invalidated by case change.
*/
private final LoadingCache<DhsImageCategory, TagName> catTagNameMap =
CacheBuilder.newBuilder().build(CacheLoader.from(
cat -> getController().getTagsManager().getTagName(cat)
));
private final LoadingCache<DhsImageCategory, TagName> catTagNameMap
= CacheBuilder.newBuilder().build(new CacheLoader<DhsImageCategory, TagName>() {
@Override
public TagName load(DhsImageCategory cat) throws TskCoreException {
return getController().getTagsManager().getTagName(cat);
}
});
public CategoryManager(ImageGalleryController controller) {
this.controller = controller;
this.drawableDb = controller.getDatabase();
}
private ImageGalleryController getController() {
return controller;
}
/**
* assign a new db. the counts cache is invalidated and all subsequent db
* lookups go to the new db.
*
* Also clears the Category TagNames
*
* @param db
*/
synchronized public void setDb(DrawableDB db) {
this.db = db;
invalidateCaches();
}
synchronized public void invalidateCaches() {
categoryCounts.invalidateAll();
catTagNameMap.invalidateAll();
@ -131,7 +121,7 @@ public class CategoryManager {
// is going on, so always use the list of file IDs we already have along with the
// other category counts instead of trying to track it separately.
long allOtherCatCount = getCategoryCount(DhsImageCategory.ONE) + getCategoryCount(DhsImageCategory.TWO) + getCategoryCount(DhsImageCategory.THREE) + getCategoryCount(DhsImageCategory.FOUR) + getCategoryCount(DhsImageCategory.FIVE);
return db.getNumberOfImageFilesInList() - allOtherCatCount;
return drawableDb.getNumberOfImageFilesInList() - allOtherCatCount;
} else {
return categoryCounts.getUnchecked(cat).sum();
}
@ -151,7 +141,7 @@ public class CategoryManager {
/**
* decrement the cached value for the number of files with the given
* {@link DhsImageCategory}
* DhsImageCategory
*
* @param cat the Category to decrement
*/
@ -175,7 +165,7 @@ public class CategoryManager {
LongAdder longAdder = new LongAdder();
longAdder.decrement();
try {
longAdder.add(db.getCategoryCount(cat));
longAdder.add(drawableDb.getCategoryCount(cat));
longAdder.increment();
} catch (IllegalStateException ex) {
LOGGER.log(Level.WARNING, "Case closed while getting files"); //NON-NLS
@ -212,15 +202,14 @@ public class CategoryManager {
try {
categoryEventBus.unregister(listener);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("missing event subscriber for an annotated method. Is " + listener + " registered?")) { //NON-NLS
/*
* We don't fully understand why we are getting this exception
* when the groups should all be registered. To avoid cluttering
* the logs we have disabled recording this exception. This
* documented in issues 738 and 802.
*/
//LOGGER.log(Level.WARNING, "Attempted to unregister {0} for category change events, but it was not registered.", listener.toString()); //NON-NLS
} else {
/*
* We don't fully understand why we are getting this exception when
* the groups should all be registered. To avoid cluttering the logs
* we have disabled recording this exception. This documented in
* issues 738 and 802.
*/
if (!e.getMessage().contains("missing event subscriber for an annotated method. Is " + listener + " registered?")) { //NON-NLS
throw e;
}
}
@ -258,7 +247,7 @@ public class CategoryManager {
//remove old category tag(s) if necessary
for (ContentTag ct : tagsManager.getContentTags(addedTag.getContent())) {
if (ct.getId() != addedTag.getId()
&& CategoryManager.isCategoryTagName(ct.getName())) {
&& CategoryManager.isCategoryTagName(ct.getName())) {
try {
tagsManager.deleteContentTag(ct);
} catch (TskCoreException tskException) {

View File

@ -18,7 +18,6 @@
*/
package org.sleuthkit.autopsy.imagegallery.datamodel;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import java.lang.ref.SoftReference;
import java.text.MessageFormat;
import java.util.ArrayList;
@ -32,6 +31,7 @@ import java.util.stream.Collectors;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.scene.image.Image;
import javafx.util.Pair;
import javax.annotation.Nonnull;
@ -40,8 +40,8 @@ import org.apache.commons.lang3.text.WordUtils;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FileTypeUtils;
import org.sleuthkit.autopsy.imagegallery.ThumbnailCache;
import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
@ -67,15 +67,21 @@ public abstract class DrawableFile {
/**
* Skip the database query if we have already determined the file type.
*
* @param file The underlying AbstractFile.
* @param analyzed Is the file analyzed.
* @param isVideo Is the file a video.
*
* @return
*/
public static DrawableFile create(AbstractFile abstractFileById, boolean analyzed, boolean isVideo) {
public static DrawableFile create(AbstractFile file, boolean analyzed, boolean isVideo) {
return isVideo
? new VideoFile(abstractFileById, analyzed)
: new ImageFile(abstractFileById, analyzed);
? new VideoFile(file, analyzed)
: new ImageFile(file, analyzed);
}
public static DrawableFile create(Long id, boolean analyzed) throws TskCoreException, NoCurrentCaseException {
return create(Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(id), analyzed);
public static DrawableFile create(Long fileID, boolean analyzed) throws TskCoreException, NoCurrentCaseException {
return create(Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(fileID), analyzed);
}
private SoftReference<Image> imageRef;
@ -149,8 +155,8 @@ public abstract class DrawableFile {
return file.getSleuthkitCase();
}
private Pair<DrawableAttribute<?>, Collection<?>> makeAttributeValuePair(DrawableAttribute<?> t) {
return new Pair<>(t, t.getValue(DrawableFile.this));
private Pair<DrawableAttribute<?>, Collection<?>> makeAttributeValuePair(DrawableAttribute<?> attribute) {
return new Pair<>(attribute, attribute.getValue(this));
}
public String getModel() {
@ -254,42 +260,17 @@ public abstract class DrawableFile {
return getSleuthkitCase().getContentTagsByContent(file);
}
@Deprecated
public Image getThumbnail() {
try {
return getThumbnailTask().get();
} catch (InterruptedException | ExecutionException ex) {
return null;
}
}
public Task<Image> getThumbnailTask() {
return ThumbnailCache.getDefault().getThumbnailTask(this);
}
@Deprecated //use non-blocking getReadFullSizeImageTask instead for most cases
public Image getFullSizeImage() {
try {
return getReadFullSizeImageTask().get();
} catch (InterruptedException | ExecutionException ex) {
return null;
}
}
public Task<Image> getReadFullSizeImageTask() {
Image image = (imageRef != null) ? imageRef.get() : null;
if (image == null || image.isError()) {
Task<Image> readImageTask = getReadFullSizeImageTaskHelper();
readImageTask.stateProperty().addListener(stateProperty -> {
switch (readImageTask.getState()) {
case SUCCEEDED:
try {
imageRef = new SoftReference<>(readImageTask.get());
} catch (InterruptedException | ExecutionException exception) {
LOGGER.log(Level.WARNING, getMessageTemplate(exception), getContentPathSafe());
}
break;
if (readImageTask.getState() == Worker.State.SUCCEEDED) {
try {
imageRef = new SoftReference<>(readImageTask.get());
} catch (InterruptedException | ExecutionException exception) {
LOGGER.log(Level.WARNING, getMessageTemplate(exception), getContentPathSafe());
}
}
});
return readImageTask;
@ -316,14 +297,14 @@ public abstract class DrawableFile {
/**
* Get the width of the visual content.
*
*
* @return The width.
*/
abstract Double getWidth();
/**
* Get the height of the visual content.
*
*
* @return The height.
*/
abstract Double getHeight();

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -18,25 +18,23 @@
*/
package org.sleuthkit.autopsy.imagegallery.datamodel;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.casemodule.services.TagsManager;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.TagName;
@ -44,39 +42,38 @@ import org.sleuthkit.datamodel.TskCoreException;
/**
* Manages Tags, Tagging, and the relationship between Categories and Tags in
* the autopsy Db. Delegates some work to the backing {@link TagsManager}.
* the autopsy Db. Delegates some work to the backing autopsy TagsManager.
*/
@NbBundle.Messages({"DrawableTagsManager.followUp=Follow Up",
"DrawableTagsManager.bookMark=Bookmark"})
public class DrawableTagsManager {
public final class DrawableTagsManager {
private static final Logger LOGGER = Logger.getLogger(DrawableTagsManager.class.getName());
private static final Logger logger = Logger.getLogger(DrawableTagsManager.class.getName());
private static Image FOLLOW_UP_IMAGE;
private static Image BOOKMARK_IMAGE;
private static final Image FOLLOW_UP_IMAGE = new Image("/org/sleuthkit/autopsy/imagegallery/images/flag_red.png");
private static final Image BOOKMARK_IMAGE = new Image("/org/sleuthkit/autopsy/images/star-bookmark-icon-16.png");
final private Object autopsyTagsManagerLock = new Object();
private TagsManager autopsyTagsManager;
private final TagsManager autopsyTagsManager;
/** The tag name corresponding to the "built-in" tag "Follow Up" */
private final TagName followUpTagName;
private final TagName bookmarkTagName;
/**
* Used to distribute {@link TagsChangeEvent}s
* Used to distribute TagsChangeEvents
*/
private final EventBus tagsEventBus = new AsyncEventBus(
Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder().namingPattern("Tags Event Bus").uncaughtExceptionHandler((Thread t, Throwable e) -> { //NON-NLS
LOGGER.log(Level.SEVERE, "uncaught exception in event bus handler", e); //NON-NLS
}).build()
));
/**
* The tag name corresponding to the "built-in" tag "Follow Up"
*/
private TagName followUpTagName;
private TagName bookmarkTagName;
public DrawableTagsManager(TagsManager autopsyTagsManager) {
this.autopsyTagsManager = autopsyTagsManager;
private final EventBus tagsEventBus
= new AsyncEventBus(
Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder()
.namingPattern("Tags Event Bus")//NON-NLS
.uncaughtExceptionHandler((Thread thread, Throwable throwable)
-> logger.log(Level.SEVERE, "Uncaught exception in DrawableTagsManager event bus handler.", throwable)) //NON-NLS
.build()));
public DrawableTagsManager(ImageGalleryController controller) throws TskCoreException {
this.autopsyTagsManager = controller.getAutopsyCase().getServices().getTagsManager();
followUpTagName = getTagName(Bundle.DrawableTagsManager_followUp());
bookmarkTagName = getTagName(Bundle.DrawableTagsManager_bookMark());
}
/**
@ -106,74 +103,51 @@ public class DrawableTagsManager {
}
/**
* assign a new TagsManager to back this one, ie when the current case
* changes
* Get the follow up TagName.
*
* @param autopsyTagsManager
* @return The follow up TagName.
*/
public void setAutopsyTagsManager(TagsManager autopsyTagsManager) {
synchronized (autopsyTagsManagerLock) {
this.autopsyTagsManager = autopsyTagsManager;
clearFollowUpTagName();
}
public TagName getFollowUpTagName() {
return followUpTagName;
}
/**
* Use when closing a case to make sure everything is re-initialized in the
* next case.
* Get the bookmark TagName.
*
* @return The bookmark TagName.
*/
public void clearFollowUpTagName() {
synchronized (autopsyTagsManagerLock) {
followUpTagName = null;
}
private TagName getBookmarkTagName() throws TskCoreException {
return bookmarkTagName;
}
/**
* get the (cached) follow up TagName
* Get all the TagNames that are not categories
*
* @return
* @return All the TagNames that are not categories, in alphabetical order
* by displayName.
*
* @throws TskCoreException
* @throws org.sleuthkit.datamodel.TskCoreException
*/
public TagName getFollowUpTagName() throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
if (Objects.isNull(followUpTagName)) {
followUpTagName = getTagName(NbBundle.getMessage(DrawableTagsManager.class, "DrawableTagsManager.followUp"));
}
return followUpTagName;
}
public List<TagName> getNonCategoryTagNames() throws TskCoreException {
return autopsyTagsManager.getAllTagNames().stream()
.filter(CategoryManager::isNotCategoryTagName)
.distinct().sorted()
.collect(Collectors.toList());
}
private Object getBookmarkTagName() throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
if (Objects.isNull(bookmarkTagName)) {
bookmarkTagName = getTagName(NbBundle.getMessage(DrawableTagsManager.class, "DrawableTagsManager.bookMark"));
}
return bookmarkTagName;
}
}
/**
* get all the TagNames that are not categories
* Get all the TagNames that are categories
*
* @return all the TagNames that are not categories, in alphabetical order
* by displayName, or, an empty set if there was an exception
* looking them up from the db.
* @return All the TagNames that are categories, in alphabetical order by
* displayName.
*
* @throws org.sleuthkit.datamodel.TskCoreException
*/
@Nonnull
public List<TagName> getNonCategoryTagNames() {
synchronized (autopsyTagsManagerLock) {
try {
return autopsyTagsManager.getAllTagNames().stream()
.filter(CategoryManager::isNotCategoryTagName)
.distinct().sorted()
.collect(Collectors.toList());
} catch (TskCoreException | IllegalStateException ex) {
LOGGER.log(Level.WARNING, "couldn't access case", ex); //NON-NLS
}
return Collections.emptyList();
}
public List<TagName> getCategoryTagNames() throws TskCoreException {
return autopsyTagsManager.getAllTagNames().stream()
.filter(CategoryManager::isCategoryTagName)
.distinct().sorted()
.collect(Collectors.toList());
}
/**
@ -187,9 +161,7 @@ public class DrawableTagsManager {
* @throws TskCoreException if there was an error reading from the db
*/
public List<ContentTag> getContentTags(Content content) throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
return autopsyTagsManager.getContentTagsByContent(content);
}
return autopsyTagsManager.getContentTagsByContent(content);
}
/**
@ -207,91 +179,56 @@ public class DrawableTagsManager {
}
public TagName getTagName(String displayName) throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
try {
TagName returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName);
if (returnTagName != null) {
return returnTagName;
}
try {
return autopsyTagsManager.addTagName(displayName);
} catch (TagsManager.TagNameAlreadyExistsException ex) {
returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName);
if (returnTagName != null) {
return returnTagName;
}
throw new TskCoreException("Tag name exists but an error occured in retrieving it", ex);
}
} catch (NullPointerException | IllegalStateException ex) {
LOGGER.log(Level.SEVERE, "Case was closed out from underneath", ex); //NON-NLS
throw new TskCoreException("Case was closed out from underneath", ex);
TagName returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName);
if (returnTagName != null) {
return returnTagName;
}
try {
return autopsyTagsManager.addTagName(displayName);
} catch (TagsManager.TagNameAlreadyExistsException ex) {
returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName);
if (returnTagName != null) {
return returnTagName;
}
throw new TskCoreException("Tag name exists but an error occured in retrieving it", ex);
}
}
public TagName getTagName(DhsImageCategory cat) {
try {
return getTagName(cat.getDisplayName());
} catch (TskCoreException ex) {
return null;
}
public TagName getTagName(DhsImageCategory cat) throws TskCoreException {
return getTagName(cat.getDisplayName());
}
public ContentTag addContentTag(DrawableFile file, TagName tagName, String comment) throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
return autopsyTagsManager.addContentTag(file.getAbstractFile(), tagName, comment);
}
return autopsyTagsManager.addContentTag(file.getAbstractFile(), tagName, comment);
}
public List<ContentTag> getContentTagsByTagName(TagName t) throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
return autopsyTagsManager.getContentTagsByTagName(t);
}
public List<ContentTag> getContentTagsByTagName(TagName tagName) throws TskCoreException {
return autopsyTagsManager.getContentTagsByTagName(tagName);
}
public List<TagName> getAllTagNames() throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
return autopsyTagsManager.getAllTagNames();
}
return autopsyTagsManager.getAllTagNames();
}
public List<TagName> getTagNamesInUse() throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
return autopsyTagsManager.getTagNamesInUse();
}
return autopsyTagsManager.getTagNamesInUse();
}
public void deleteContentTag(ContentTag ct) throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
autopsyTagsManager.deleteContentTag(ct);
}
public void deleteContentTag(ContentTag contentTag) throws TskCoreException {
autopsyTagsManager.deleteContentTag(contentTag);
}
public Node getGraphic(TagName tagname) {
try {
if (tagname.equals(getFollowUpTagName())) {
return new ImageView(getFollowUpImage());
return new ImageView(FOLLOW_UP_IMAGE);
} else if (tagname.equals(getBookmarkTagName())) {
return new ImageView(getBookmarkImage());
return new ImageView(BOOKMARK_IMAGE);
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to get \"Follow Up\" or \"Bookmark\"tag name from db.", ex);
logger.log(Level.SEVERE, "Failed to get \"Follow Up\" or \"Bookmark\"tag name from db.", ex);
}
return DrawableAttribute.TAGS.getGraphicForValue(tagname);
}
synchronized private static Image getFollowUpImage() {
if (FOLLOW_UP_IMAGE == null) {
FOLLOW_UP_IMAGE = new Image("/org/sleuthkit/autopsy/imagegallery/images/flag_red.png");
}
return FOLLOW_UP_IMAGE;
}
synchronized private static Image getBookmarkImage() {
if (BOOKMARK_IMAGE == null) {
BOOKMARK_IMAGE = new Image("/org/sleuthkit/autopsy/images/star-bookmark-icon-16.png");
}
return BOOKMARK_IMAGE;
}
}

View File

@ -1,8 +1,26 @@
package org.sleuthkit.autopsy.imagegallery.datamodel;
/*
* Autopsy Forensic Browser
*
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/package org.sleuthkit.autopsy.imagegallery.datamodel;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Set;
import java.util.logging.Level;
@ -15,26 +33,18 @@ import org.sleuthkit.datamodel.TskCoreException;
*/
public class HashSetManager {
/**
* The db that initial values are loaded from.
*/
private DrawableDB db = null;
/** The db that initial values are loaded from. */
private final DrawableDB drawableDB;
public HashSetManager(DrawableDB drawableDB) {
this.drawableDB = drawableDB;
}
/**
* the internal cache from fileID to a set of hashset names.
*/
private final LoadingCache<Long, Set<String>> hashSetCache = CacheBuilder.newBuilder().build(CacheLoader.from(this::getHashSetsForFileHelper));
/**
* assign the given db to back this hashset manager.
*
* @param db
*/
public void setDb(DrawableDB db) {
this.db = db;
hashSetCache.invalidateAll();
}
/**
* helper method to load hashset hits for the given fileID from the db
*
@ -44,9 +54,14 @@ public class HashSetManager {
*/
private Set<String> getHashSetsForFileHelper(long fileID) {
try {
return db.getHashSetsForFile(fileID);
} catch (TskCoreException ex) {
Logger.getLogger(HashSetManager.class.getName()).log(Level.SEVERE, "Failed to get Hash Sets for file", ex); //NON-NLS
if (drawableDB.isClosed()) {
Logger.getLogger(HashSetManager.class.getName()).log(Level.WARNING, "Failed to get Hash Sets for file. The Db connection was already closed."); //NON-NLS
return Collections.emptySet();
} else {
return drawableDB.getHashSetsForFile(fileID);
}
} catch (TskCoreException | SQLException ex) {
Logger.getLogger(HashSetManager.class.getName()).log(Level.SEVERE, "Failed to get Hash Sets for file."); //NON-NLS
return Collections.emptySet();
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -26,16 +26,19 @@ 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;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Represents a set of image/video files in a group. The UI listens to changes
@ -76,7 +79,7 @@ public class DrawableGroup implements Comparable<DrawableGroup> {
}
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public synchronized ObservableList<Long> getFileIDs() {
public ObservableList<Long> getFileIDs() {
return unmodifiableFileIDS;
}
@ -121,11 +124,11 @@ public class DrawableGroup implements Comparable<DrawableGroup> {
if (hashSetHitsCount.get() < 0) {
try {
hashSetHitsCount.set(fileIDs.stream()
.map(fileID -> ImageGalleryController.getDefault().getHashSetManager().isInAnyHashSet(fileID))
.map(ImageGalleryModule.getController().getHashSetManager()::isInAnyHashSet)
.filter(Boolean::booleanValue)
.count());
} catch (IllegalStateException | NullPointerException ex) {
LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
} catch (NoCurrentCaseException ex) {
LOGGER.log(Level.WARNING, "Could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
}
}
return hashSetHitsCount.get();
@ -139,10 +142,10 @@ public class DrawableGroup implements Comparable<DrawableGroup> {
public final synchronized long getUncategorizedCount() {
if (uncatCount.get() < 0) {
try {
uncatCount.set(ImageGalleryController.getDefault().getDatabase().getUncategorizedCount(fileIDs));
uncatCount.set(ImageGalleryModule.getController().getDatabase().getUncategorizedCount(fileIDs));
} catch (IllegalStateException | NullPointerException ex) {
LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
} catch (TskCoreException | NoCurrentCaseException ex) {
LOGGER.log(Level.WARNING, "Could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
}
}
@ -163,8 +166,8 @@ public class DrawableGroup implements Comparable<DrawableGroup> {
return seen.get();
}
public ReadOnlyBooleanWrapper seenProperty() {
return seen;
public ReadOnlyBooleanProperty seenProperty() {
return seen.getReadOnlyProperty();
}
@Subscribe

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -18,27 +18,31 @@
*/
package org.sleuthkit.autopsy.imagegallery.datamodel.grouping;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javafx.scene.Node;
import javax.annotation.concurrent.Immutable;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.TagName;
/**
* key identifying information of a {@link Grouping}. Used to look up groups in
* {@link Map}s and from the db.
* Key identifying information of a DrawableGroup. Used to look up groups in
* Maps and from the db.
*
* @param <T> The type of the values of the attribute this key uses.
*/
@Immutable
public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>> {
private final T val;
private final DrawableAttribute<T> attr;
private final DataSource dataSource;
public GroupKey(DrawableAttribute<T> attr, T val) {
public GroupKey(DrawableAttribute<T> attr, T val, DataSource dataSource) {
this.attr = attr;
this.val = val;
this.dataSource = dataSource;
}
public T getValue() {
@ -49,6 +53,10 @@ public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>
return attr;
}
public Optional< DataSource> getDataSource() {
return Optional.ofNullable(dataSource);
}
public String getValueDisplayName() {
return Objects.equals(attr, DrawableAttribute.TAGS)
? ((TagName) getValue()).getDisplayName()
@ -63,13 +71,19 @@ public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>
@Override
public int hashCode() {
int hash = 5;
hash = 29 * hash + Objects.hashCode(this.val);
hash = 29 * hash + Objects.hashCode(this.attr);
hash = 79 * hash + Objects.hashCode(this.val);
hash = 79 * hash + Objects.hashCode(this.attr);
hash = 79 * hash + Objects.hashCode(this.dataSource);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
@ -77,11 +91,14 @@ public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>
return false;
}
final GroupKey<?> other = (GroupKey<?>) obj;
if (this.attr != other.attr) {
if (!Objects.equals(this.val, other.val)) {
return false;
}
return Objects.equals(this.val, other.val);
if (!Objects.equals(this.attr, other.attr)) {
return false;
}
return Objects.equals(this.dataSource, other.dataSource);
}
@Override
@ -92,4 +109,8 @@ public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>
public Node getGraphic() {
return attr.getGraphicForValue(val);
}
public long getDataSourceObjId() {
return getDataSource().map(DataSource::getId).orElse(0L);
}
}

View File

@ -28,7 +28,8 @@ import org.openide.util.NbBundle;
/**
* Pseudo enum of possible properties to sort groups by.
*/
@NbBundle.Messages({"GroupSortBy.groupSize=Group Size",
@NbBundle.Messages({
"GroupSortBy.groupSize=Group Size",
"GroupSortBy.groupName=Group Name",
"GroupSortBy.none=None",
"GroupSortBy.priority=Priority"})
@ -37,40 +38,35 @@ public class GroupSortBy implements Comparator<DrawableGroup> {
/**
* sort the groups by the number of files in each
*/
public final static GroupSortBy FILE_COUNT =
new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png",
public final static GroupSortBy FILE_COUNT
= new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png",
Comparator.comparing(DrawableGroup::getSize));
/**
* sort the groups by the natural order of the grouping value ( eg group
* them by path alphabetically )
*/
public final static GroupSortBy GROUP_BY_VALUE =
new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png",
public final static GroupSortBy GROUP_BY_VALUE
= new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png",
Comparator.comparing(DrawableGroup::getGroupByValueDislpayName));
/**
* don't sort the groups just use what ever order they come in (ingest
* order)
*/
public final static GroupSortBy NONE =
new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png",
public final static GroupSortBy NONE
= new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png",
new AllEqualComparator<>());
/**
* sort the groups by some priority metric to be determined and implemented
*/
public final static GroupSortBy PRIORITY =
new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png",
public final static GroupSortBy PRIORITY
= new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png",
Comparator.comparing(DrawableGroup::getHashHitDensity)
.thenComparing(Comparator.comparing(DrawableGroup::getUncategorizedCount))
.reversed());
@Override
public int compare(DrawableGroup o1, DrawableGroup o2) {
return delegate.compare(o1, o2);
}
private final static ObservableList<GroupSortBy> values = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(PRIORITY, NONE, GROUP_BY_VALUE, FILE_COUNT));
/**
@ -109,6 +105,11 @@ public class GroupSortBy implements Comparator<DrawableGroup> {
return icon;
}
@Override
public int compare(DrawableGroup o1, DrawableGroup o2) {
return delegate.compare(o1, o2);
}
static class AllEqualComparator<A> implements Comparator<A> {
@Override

View File

@ -22,9 +22,9 @@ import java.util.Objects;
import java.util.Optional;
/**
*
* Encapsulate information about the state of the group section of the UI.
*/
public class GroupViewState {
public final class GroupViewState {
private final DrawableGroup group;
@ -32,8 +32,8 @@ public class GroupViewState {
private final Optional<Long> slideShowfileID;
public DrawableGroup getGroup() {
return group;
public Optional<DrawableGroup> getGroup() {
return Optional.ofNullable(group);
}
public GroupViewMode getMode() {
@ -44,18 +44,18 @@ public class GroupViewState {
return slideShowfileID;
}
private GroupViewState(DrawableGroup g, GroupViewMode mode, Long slideShowfileID) {
this.group = g;
private GroupViewState(DrawableGroup group, GroupViewMode mode, Long slideShowfileID) {
this.group = group;
this.mode = mode;
this.slideShowfileID = Optional.ofNullable(slideShowfileID);
}
public static GroupViewState tile(DrawableGroup g) {
return new GroupViewState(g, GroupViewMode.TILE, null);
public static GroupViewState tile(DrawableGroup group) {
return new GroupViewState(group, GroupViewMode.TILE, null);
}
public static GroupViewState slideShow(DrawableGroup g, Long fileID) {
return new GroupViewState(g, GroupViewMode.SLIDE_SHOW, fileID);
public static GroupViewState slideShow(DrawableGroup group, Long fileID) {
return new GroupViewState(group, GroupViewMode.SLIDE_SHOW, fileID);
}
@Override
@ -82,10 +82,7 @@ public class GroupViewState {
if (this.mode != other.mode) {
return false;
}
if (!Objects.equals(this.slideShowfileID, other.slideShowfileID)) {
return false;
}
return true;
return Objects.equals(this.slideShowfileID, other.slideShowfileID);
}
}

View File

@ -18,20 +18,43 @@
*/
package org.sleuthkit.autopsy.imagegallery.gui;
import java.io.IOException;
import java.net.URL;
import java.util.logging.Level;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Dialog;
import javafx.scene.control.MenuItem;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.controlsfx.control.action.Action;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
/**
* Static utility methods for working with GUI components
*/
public class GuiUtils {
public final class GuiUtils {
private final static Logger logger = Logger.getLogger(GuiUtils.class.getName());
/** Image to use as title bar icon in dialogs */
private static final Image AUTOPSY_ICON;
static {
Image tempImg = null;
try {
tempImg = new Image(new URL("nbresloc:/org/netbeans/core/startup/frame.gif").openStream()); //NON-NLS
} catch (IOException ex) {
logger.log(Level.WARNING, "Failed to load branded icon for progress dialog.", ex); //NON-NLS
}
AUTOPSY_ICON = tempImg;
}
private GuiUtils() {
}
/**
* create a MenuItem that performs the given action and also set the Action
* Create a MenuItem that performs the given action and also set the Action
* as the action for the given Button. Usefull to have a SplitMenuButton
* remember the last chosen menu item as its action.
*
@ -51,4 +74,14 @@ public class GuiUtils {
});
return menuItem;
}
/**
* Set the title bar icon for the given Dialog to be the Autopsy logo icon.
*
* @param dialog The dialog to set the title bar icon for.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
public static void setDialogIcons(Dialog<?> dialog) {
((Stage) dialog.getDialogPane().getScene().getWindow()).getIcons().setAll(AUTOPSY_ICON);
}
}

View File

@ -79,7 +79,7 @@ public class StatusBar extends AnchorPane {
});
Platform.runLater(() -> staleLabel.setTooltip(new Tooltip(Bundle.StatuBar_toolTip())));
staleLabel.visibleProperty().bind(controller.stale());
staleLabel.visibleProperty().bind(controller.staleProperty());
}
public StatusBar(ImageGalleryController controller) {

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-15 Basis Technology Corp.
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -34,9 +34,10 @@ import javafx.scene.layout.VBox;
import javafx.util.Pair;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager.CategoryChangeEvent;
/**
* Displays summary statistics (counts) for each group
@ -51,11 +52,13 @@ public class SummaryTablePane extends AnchorPane {
@FXML
private TableView<Pair<DhsImageCategory, Long>> tableView;
private final ImageGalleryController controller;
@FXML
@NbBundle.Messages({"SummaryTablePane.catColumn=Category",
"SummaryTablePane.countColumn=# Files"})
@NbBundle.Messages({
"SummaryTablePane.catColumn=Category",
"SummaryTablePane.countColumn=# Files"})
void initialize() {
assert catColumn != null : "fx:id=\"catColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'.";
assert countColumn != null : "fx:id=\"countColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'.";
@ -67,11 +70,11 @@ public class SummaryTablePane extends AnchorPane {
tableView.prefHeightProperty().set(7 * 25);
//set up columns
catColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<DhsImageCategory, Long>, String> p) -> new SimpleObjectProperty<>(p.getValue().getKey().getDisplayName()));
catColumn.setCellValueFactory(params -> new SimpleObjectProperty<>(params.getValue().getKey().getDisplayName()));
catColumn.setPrefWidth(USE_COMPUTED_SIZE);
catColumn.setText(Bundle.SummaryTablePane_catColumn());
countColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<DhsImageCategory, Long>, Long> p) -> new SimpleObjectProperty<>(p.getValue().getValue()));
countColumn.setCellValueFactory(params -> new SimpleObjectProperty<>(params.getValue().getValue()));
countColumn.setPrefWidth(USE_COMPUTED_SIZE);
countColumn.setText(Bundle.SummaryTablePane_countColumn());
@ -85,14 +88,15 @@ public class SummaryTablePane extends AnchorPane {
public SummaryTablePane(ImageGalleryController controller) {
this.controller = controller;
FXMLConstructor.construct(this, "SummaryTablePane.fxml"); //NON-NLS
}
/**
* listen to Category updates and rebuild the table
*
* @param evt The change event.
*/
@Subscribe
public void handleCategoryChanged(org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager.CategoryChangeEvent evt) {
public void handleCategoryChanged(CategoryChangeEvent evt) {
final ObservableList<Pair<DhsImageCategory, Long>> data = FXCollections.observableArrayList();
if (Case.isCaseOpen()) {
for (DhsImageCategory cat : DhsImageCategory.values()) {

View File

@ -12,29 +12,41 @@
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?>
<fx:root minWidth="-1.0" orientation="HORIZONTAL" prefWidth="-1.0" type="javafx.scene.control.ToolBar" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
<items>
<fx:root minWidth="-1.0" orientation="HORIZONTAL" prefWidth="-1.0" type="javafx.scene.control.ToolBar" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1">
<items>
<HBox alignment="CENTER" spacing="5.0">
<children>
<Label fx:id="groupByLabel" text="Group By:">
<labelFor>
<ComboBox fx:id="groupByBox" editable="false" />
</labelFor>
</Label>
<fx:reference source="groupByBox" />
<ComboBox fx:id="groupByBox" prefWidth="100.0" />
</children>
</HBox>
<ImageView fx:id="sortHelpImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../images/question-frame.png" />
</image>
</ImageView>
<HBox alignment="CENTER" spacing="5.0">
<children>
<Label mnemonicParsing="false" text=" Data Source: ">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../images/datasource.png" />
</image>
</ImageView>
</graphic>
</Label>
<ComboBox fx:id="dataSourceComboBox" editable="false" maxWidth="200.0" />
</children>
</HBox>
<ImageView fx:id="sortHelpImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../images/question-frame.png" />
</image>
</ImageView>
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="10.0" />
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" />
<HBox alignment="CENTER" spacing="5.0">
<children>
<Label fx:id="tagImageViewLabel" text="Tag Group's Files:">
@ -76,7 +88,7 @@
<Insets left="5.0" />
</padding>
</HBox>
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="10.0" />
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" />
<HBox alignment="CENTER" spacing="5.0">
<children>
<Label fx:id="thumbnailSizeLabel" text="Thumbnail Size (px):">

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -18,22 +18,33 @@
*/
package org.sleuthkit.autopsy.imagegallery.gui;
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 com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SingleSelectionModel;
import javafx.scene.control.Slider;
import javafx.scene.control.SplitMenuButton;
import javafx.scene.control.ToolBar;
@ -42,63 +53,70 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.text.Text;
import javafx.stage.Modality;
import javafx.util.StringConverter;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import org.controlsfx.control.PopOver;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import static org.sleuthkit.autopsy.casemodule.Case.Events.DATA_SOURCE_ADDED;
import static org.sleuthkit.autopsy.casemodule.Case.Events.DATA_SOURCE_DELETED;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.actions.CategorizeGroupAction;
import org.sleuthkit.autopsy.imagegallery.actions.TagGroupAction;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
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.datamodel.TskCoreException;
import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils;
import org.sleuthkit.datamodel.DataSource;
/**
* Controller for the ToolBar
*/
public class Toolbar extends ToolBar {
private static final Logger LOGGER = Logger.getLogger(Toolbar.class.getName());
private static final Logger logger = Logger.getLogger(Toolbar.class.getName());
private static final int SIZE_SLIDER_DEFAULT = 100;
@FXML
private ComboBox<Optional<DataSource>> dataSourceComboBox;
@FXML
private ImageView sortHelpImageView;
@FXML
private ComboBox<DrawableAttribute<?>> groupByBox;
@FXML
private Slider sizeSlider;
@FXML
private SplitMenuButton catGroupMenuButton;
@FXML
private SplitMenuButton tagGroupMenuButton;
@FXML
private Label groupByLabel;
@FXML
private Label tagImageViewLabel;
@FXML
private Label categoryImageViewLabel;
@FXML
private Label thumbnailSizeLabel;
private final ImageGalleryController controller;
private SortChooser<DrawableGroup, GroupSortBy> sortChooser;
private final ListeningExecutorService exec = TaskUtils.getExecutorForClass(Toolbar.class);
private final ImageGalleryController controller;
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final ObservableList<Optional<DataSource>> dataSources = FXCollections.observableArrayList();
private SingleSelectionModel<Optional<DataSource>> dataSourceSelectionModel;
private final InvalidationListener queryInvalidationListener = new InvalidationListener() {
public void invalidated(Observable o) {
controller.getGroupManager().regroup(
@Override
public void invalidated(Observable invalidated) {
controller.getGroupManager().regroup(getSelectedDataSource(),
groupByBox.getSelectionModel().getSelectedItem(),
sortChooser.getComparator(),
sortChooser.getSortOrder(),
@ -106,61 +124,94 @@ public class Toolbar extends ToolBar {
}
};
public DoubleProperty thumbnailSizeProperty() {
return sizeSlider.valueProperty();
}
@FXML
@NbBundle.Messages({"Toolbar.groupByLabel=Group By:",
"Toolbar.sortByLabel=Sort By:",
"Toolbar.ascRadio=Ascending",
"Toolbar.descRadio=Descending",
"Toolbar.tagImageViewLabel=Tag Group's Files:",
"Toolbar.categoryImageViewLabel=Categorize Group's Files:",
"Toolbar.thumbnailSizeLabel=Thumbnail Size (px):",
"Toolbar.sortHelp=The sort direction (ascending/descending) affects the queue of unseen groups that Image Gallery maintains, but changes to this queue aren't apparent until the \"Next Unseen Group\" button is pressed.",
"Toolbar.sortHelpTitle=Group Sorting",})
@NbBundle.Messages(
{"Toolbar.groupByLabel=Group By:",
"Toolbar.sortByLabel=Sort By:",
"Toolbar.ascRadio=Ascending",
"Toolbar.descRadio=Descending",
"Toolbar.tagImageViewLabel=Tag Group's Files:",
"Toolbar.categoryImageViewLabel=Categorize Group's Files:",
"Toolbar.thumbnailSizeLabel=Thumbnail Size (px):",
"Toolbar.sortHelp=The sort direction (ascending/descending) affects the queue of unseen groups that Image Gallery maintains, but changes to this queue aren't apparent until the \"Next Unseen Group\" button is pressed.",
"Toolbar.sortHelpTitle=Group Sorting",
"Toolbar.getDataSources.errMessage=Unable to get datasources for current case.",
"Toolbar.nonPathGroupingWarning.content=Proceed with regrouping?",
"Toolbar.nonPathGroupingWarning.header=Grouping by attributes other than path does not support the data source filter.\nFiles and groups from all data sources will be shown.",
"Toolbar.nonPathGroupingWarning.title=Image Gallery"})
void initialize() {
assert catGroupMenuButton != null : "fx:id=\"catSelectedMenubutton\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert groupByBox != null : "fx:id=\"groupByBox\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert dataSourceComboBox != null : "fx:id=\"dataSourceComboBox\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert sortHelpImageView != null : "fx:id=\"sortHelpImageView\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert tagImageViewLabel != null : "fx:id=\"tagImageViewLabel\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert tagGroupMenuButton != null : "fx:id=\"tagGroupMenuButton\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert categoryImageViewLabel != null : "fx:id=\"categoryImageViewLabel\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert catGroupMenuButton != null : "fx:id=\"catGroupMenuButton\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert thumbnailSizeLabel != null : "fx:id=\"thumbnailSizeLabel\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert sizeSlider != null : "fx:id=\"sizeSlider\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert tagGroupMenuButton != null : "fx:id=\"tagSelectedMenubutton\" was not injected: check your FXML file 'Toolbar.fxml'.";
this.dataSourceSelectionModel = dataSourceComboBox.getSelectionModel();
controller.viewState().addListener((observable, oldViewState, newViewState) -> {
Platform.runLater(() -> syncGroupControlsEnabledState(newViewState));
});
syncGroupControlsEnabledState(controller.viewState().get());
//set internationalized label text
groupByLabel.setText(Bundle.Toolbar_groupByLabel());
tagImageViewLabel.setText(Bundle.Toolbar_tagImageViewLabel());
categoryImageViewLabel.setText(Bundle.Toolbar_categoryImageViewLabel());
thumbnailSizeLabel.setText(Bundle.Toolbar_thumbnailSizeLabel());
sizeSlider.valueProperty().bindBidirectional(controller.thumbnailSizeProperty());
controller.viewStateProperty().addListener((observable, oldViewState, newViewState)
-> Platform.runLater(() -> syncGroupControlsEnabledState(newViewState))
);
syncGroupControlsEnabledState(controller.viewStateProperty().get());
try {
TagGroupAction followUpGroupAction = new TagGroupAction(controller.getTagsManager().getFollowUpTagName(), controller);
tagGroupMenuButton.setOnAction(followUpGroupAction);
tagGroupMenuButton.setText(followUpGroupAction.getText());
tagGroupMenuButton.setGraphic(followUpGroupAction.getGraphic());
} catch (TskCoreException ex) {
/*
* 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", ex); //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
}
}
tagGroupMenuButton.showingProperty().addListener(showing -> {
if (tagGroupMenuButton.isShowing()) {
List<MenuItem> selTagMenues = Lists.transform(controller.getTagsManager().getNonCategoryTagNames(),
tn -> GuiUtils.createAutoAssigningMenuItem(tagGroupMenuButton, new TagGroupAction(tn, controller)));
tagGroupMenuButton.getItems().setAll(selTagMenues);
initDataSourceComboBox();
groupByBox.setItems(FXCollections.observableList(DrawableAttribute.getGroupableAttrs()));
groupByBox.getSelectionModel().select(DrawableAttribute.PATH);
groupByBox.disableProperty().bind(controller.regroupDisabledProperty());
groupByBox.setCellFactory(listView -> new AttributeListCell());
groupByBox.setButtonCell(new AttributeListCell());
groupByBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue == DrawableAttribute.PATH
&& newValue != DrawableAttribute.PATH
&& getSelectedDataSource() != null) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, Bundle.Toolbar_nonPathGroupingWarning_content());
alert.setHeaderText(Bundle.Toolbar_nonPathGroupingWarning_header());
alert.setTitle(Bundle.Toolbar_nonPathGroupingWarning_title());
alert.initModality(Modality.APPLICATION_MODAL);
alert.initOwner(getScene().getWindow());
GuiUtils.setDialogIcons(alert);
if (alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
queryInvalidationListener.invalidated(observable);
} else {
Platform.runLater(() -> groupByBox.getSelectionModel().select(DrawableAttribute.PATH));
}
} else {
queryInvalidationListener.invalidated(observable);
}
});
sortChooser = new SortChooser<>(GroupSortBy.getValues());
sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> {
final boolean orderDisabled = newComparator == GroupSortBy.NONE || newComparator == GroupSortBy.PRIORITY;
sortChooser.setSortOrderDisabled(orderDisabled);
final SortChooser.ValueType valueType = newComparator == GroupSortBy.GROUP_BY_VALUE ? SortChooser.ValueType.LEXICOGRAPHIC : SortChooser.ValueType.NUMERIC;
sortChooser.setValueType(valueType);
queryInvalidationListener.invalidated(observable);
});
sortChooser.setComparator(controller.getGroupManager().getSortBy());
sortChooser.sortOrderProperty().addListener(queryInvalidationListener);
getItems().add(2, sortChooser);
sortHelpImageView.setCursor(Cursor.HAND);
sortHelpImageView.setOnMouseClicked(clicked -> {
Text text = new Text(Bundle.Toolbar_sortHelp());
text.setWrappingWidth(480); //This is a hack to fix the layout.
showPopoverHelp(sortHelpImageView,
Bundle.Toolbar_sortHelpTitle(),
sortHelpImageView.getImage(), text);
});
initTagMenuButton();
CategorizeGroupAction cat5GroupAction = new CategorizeGroupAction(DhsImageCategory.FIVE, controller);
catGroupMenuButton.setOnAction(cat5GroupAction);
catGroupMenuButton.setText(cat5GroupAction.getText());
@ -173,41 +224,113 @@ public class Toolbar extends ToolBar {
}
});
groupByLabel.setText(Bundle.Toolbar_groupByLabel());
tagImageViewLabel.setText(Bundle.Toolbar_tagImageViewLabel());
categoryImageViewLabel.setText(Bundle.Toolbar_categoryImageViewLabel());
thumbnailSizeLabel.setText(Bundle.Toolbar_thumbnailSizeLabel());
}
groupByBox.setItems(FXCollections.observableList(DrawableAttribute.getGroupableAttrs()));
groupByBox.getSelectionModel().select(DrawableAttribute.PATH);
groupByBox.getSelectionModel().selectedItemProperty().addListener(queryInvalidationListener);
groupByBox.disableProperty().bind(ImageGalleryController.getDefault().regroupDisabled());
groupByBox.setCellFactory(listView -> new AttributeListCell());
groupByBox.setButtonCell(new AttributeListCell());
private DataSource getSelectedDataSource() {
Optional<DataSource> empty = Optional.empty();
return defaultIfNull(dataSourceSelectionModel.getSelectedItem(), empty).orElse(null);
}
sortChooser = new SortChooser<>(GroupSortBy.getValues());
sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> {
final boolean orderDisabled = newComparator == GroupSortBy.NONE || newComparator == GroupSortBy.PRIORITY;
sortChooser.setSortOrderDisabled(orderDisabled);
private void initDataSourceComboBox() {
dataSourceComboBox.setCellFactory(param -> new DataSourceCell());
dataSourceComboBox.setButtonCell(new DataSourceCell());
dataSourceComboBox.setConverter(new StringConverter<Optional<DataSource>>() {
@Override
public String toString(Optional<DataSource> object) {
return object.map(DataSource::getName).orElse("All");
}
final SortChooser.ValueType valueType = newComparator == GroupSortBy.GROUP_BY_VALUE ? SortChooser.ValueType.LEXICOGRAPHIC : SortChooser.ValueType.NUMERIC;
sortChooser.setValueType(valueType);
queryInvalidationListener.invalidated(observable);
@Override
public Optional<DataSource> fromString(String string) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
});
dataSourceComboBox.setItems(dataSources);
sortChooser.sortOrderProperty().addListener(queryInvalidationListener);
sortChooser.setComparator(controller.getGroupManager().getSortBy());
getItems().add(1, sortChooser);
sortHelpImageView.setCursor(Cursor.HAND);
Case.addEventTypeSubscriber(ImmutableSet.of(DATA_SOURCE_ADDED, DATA_SOURCE_DELETED),
evt -> {
Platform.runLater(() -> {
Optional<DataSource> selectedItem = dataSourceSelectionModel.getSelectedItem();
syncDataSources().addListener(() -> dataSourceSelectionModel.select(selectedItem), Platform::runLater);
});
});
syncDataSources();
sortHelpImageView.setOnMouseClicked(clicked -> {
Text text = new Text(Bundle.Toolbar_sortHelp());
text.setWrappingWidth(480); //This is a hack to fix the layout.
showPopoverHelp(sortHelpImageView,
Bundle.Toolbar_sortHelpTitle(),
sortHelpImageView.getImage(), text);
controller.getGroupManager().getDataSourceProperty()
.addListener((observable, oldDataSource, newDataSource)
-> dataSourceSelectionModel.select(Optional.ofNullable(newDataSource)));
dataSourceSelectionModel.select(Optional.ofNullable(controller.getGroupManager().getDataSource()));
dataSourceComboBox.disableProperty().bind(groupByBox.getSelectionModel().selectedItemProperty().isNotEqualTo(DrawableAttribute.PATH));
dataSourceSelectionModel.selectedItemProperty().addListener(queryInvalidationListener);
}
private void initTagMenuButton() {
ListenableFuture<TagGroupAction> future = exec.submit(() -> new TagGroupAction(controller.getTagsManager().getFollowUpTagName(), controller));
Futures.addCallback(future, new FutureCallback<TagGroupAction>() {
@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
}
}
}, Platform::runLater);
tagGroupMenuButton.showingProperty().addListener(showing -> {
if (tagGroupMenuButton.isShowing()) {
ListenableFuture<List<MenuItem>> getTagsFuture = exec.submit(() -> {
return Lists.transform(controller.getTagsManager().getNonCategoryTagNames(),
tagName -> GuiUtils.createAutoAssigningMenuItem(tagGroupMenuButton, new TagGroupAction(tagName, controller)));
});
Futures.addCallback(getTagsFuture, new FutureCallback<List<MenuItem>>() {
@Override
public void onSuccess(List<MenuItem> result) {
tagGroupMenuButton.getItems().setAll(result);
}
@Override
public void onFailure(Throwable t) {
logger.log(Level.SEVERE, "Error getting non-gategory tag names.", t);
}
}, Platform::runLater);
}
});
}
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
private ListenableFuture<List<DataSource>> syncDataSources() {
ListenableFuture<List<DataSource>> future = exec.submit(controller.getSleuthKitCase()::getDataSources);
Futures.addCallback(future, new FutureCallback<List<DataSource>>() {
@Override
public void onSuccess(List<DataSource> result) {
dataSources.setAll(Collections.singleton(Optional.empty()));
result.forEach(dataSource -> dataSources.add(Optional.of(dataSource)));
}
@Override
public void onFailure(Throwable t) {
logger.log(Level.SEVERE, "Unable to get datasources for current case.", t); //NON-NLS
}
}, Platform::runLater);
return future;
}
/**
@ -237,13 +360,20 @@ public class Toolbar extends ToolBar {
popOver.show(owner);
}
/**
* Disable the tag and catagory controls if and only if there is no group
* selected.
*
* @param newViewState The GroupViewState to use as a source of the
* selection.
*/
private void syncGroupControlsEnabledState(GroupViewState newViewState) {
boolean noGroupSelected = newViewState == null
? true
: newViewState.getGroup() == null;
tagGroupMenuButton.setDisable(noGroupSelected);
catGroupMenuButton.setDisable(noGroupSelected);
boolean noGroupSelected = (null == newViewState)
|| (null == newViewState.getGroup());
Platform.runLater(() -> {
tagGroupMenuButton.setDisable(noGroupSelected);
catGroupMenuButton.setDisable(noGroupSelected);
});
}
public void reset() {
@ -256,5 +386,22 @@ public class Toolbar extends ToolBar {
public Toolbar(ImageGalleryController controller) {
this.controller = controller;
FXMLConstructor.construct(this, "Toolbar.fxml"); //NON-NLS
}
/**
* Cell used to represent a DataSource in the dataSourceComboBoc
*/
static private class DataSourceCell extends ListCell<Optional<DataSource>> {
@Override
protected void updateItem(Optional<DataSource> item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText("All");
} else {
setText(item.map(DataSource::getName).orElse("All"));
}
}
}
}

View File

@ -106,7 +106,7 @@ public class DrawableTile extends DrawableTileBase {
@Override
Task<Image> newReadImageTask(DrawableFile file) {
return file.getThumbnailTask();
return getController().getThumbsCache().getThumbnailTask(file);
}
@Override

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-2017 Basis Technology Corp.
* Copyright 2013-2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -79,9 +79,9 @@ import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException;
/**
* An abstract base class for {@link DrawableTile} and {@link SlideShowView},
* since they share a similar node tree and many behaviors, other implementors
* of {@link DrawableView}s should implement the interface directly
* An abstract base class for DrawableTile and SlideShowView, since they share a
* similar node tree and many behaviors, other implementors of DrawableViews
* should implement the interface directly
*
*
* TODO: refactor ExternalViewerAction to supply its own name
@ -89,7 +89,7 @@ import org.sleuthkit.datamodel.TskCoreException;
@NbBundle.Messages({"DrawableTileBase.externalViewerAction.text=Open in External Viewer"})
public abstract class DrawableTileBase extends DrawableUIBase {
private static final Logger LOGGER = Logger.getLogger(DrawableTileBase.class.getName());
private static final Logger logger = Logger.getLogger(DrawableTileBase.class.getName());
private static final Border UNSELECTED_BORDER = new Border(new BorderStroke(Color.GRAY, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3)));
private static final Border SELECTED_BORDER = new Border(new BorderStroke(Color.BLUE, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3)));
@ -187,28 +187,32 @@ public abstract class DrawableTileBase extends DrawableUIBase {
final ArrayList<MenuItem> menuItems = new ArrayList<>();
menuItems.add(CategorizeAction.getCategoriesMenu(getController()));
menuItems.add(AddTagAction.getTagMenu(getController()));
try {
menuItems.add(AddTagAction.getTagMenu(getController()));
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error building tagging context menu.", ex);
}
final Collection<AbstractFile> selectedFilesList = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class));
if(selectedFilesList.size() == 1) {
if (selectedFilesList.size() == 1) {
menuItems.add(DeleteTagAction.getTagMenu(getController()));
}
final MenuItem extractMenuItem = new MenuItem(Bundle.DrawableTileBase_menuItem_extractFiles());
extractMenuItem.setOnAction(actionEvent -> {
SwingUtilities.invokeLater(() -> {
TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID);
ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null));
});
});
extractMenuItem.setOnAction(actionEvent
-> SwingUtilities.invokeLater(() -> {
TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID);
ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null));
}));
menuItems.add(extractMenuItem);
MenuItem contentViewer = new MenuItem(Bundle.DrawableTileBase_menuItem_showContentViewer());
contentViewer.setOnAction(actionEvent -> {
SwingUtilities.invokeLater(() -> {
new NewWindowViewAction(Bundle.DrawableTileBase_menuItem_showContentViewer(), new FileNode(file.getAbstractFile())).actionPerformed(null);
});
});
contentViewer.setOnAction(actionEvent
-> SwingUtilities.invokeLater(() -> {
new NewWindowViewAction(Bundle.DrawableTileBase_menuItem_showContentViewer(), new FileNode(file.getAbstractFile()))
.actionPerformed(null);
}));
menuItems.add(contentViewer);
OpenExternalViewerAction openExternalViewerAction = new OpenExternalViewerAction(file);
@ -243,35 +247,32 @@ public abstract class DrawableTileBase extends DrawableUIBase {
protected abstract String getTextForLabel();
protected void initialize() {
followUpToggle.setOnAction(actionEvent -> {
getFile().ifPresent(file -> {
if (followUpToggle.isSelected() == true) {
try {
selectionModel.clearAndSelect(file.getId());
new AddTagAction(getController(), getController().getTagsManager().getFollowUpTagName(), selectionModel.getSelected()).handle(actionEvent);
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to add Follow Up tag. Could not load TagName.", ex); //NON-NLS
}
} else {
new DeleteFollowUpTagAction(getController(), file).handle(actionEvent);
}
});
});
followUpToggle.setOnAction(
actionEvent -> getFile().ifPresent(
file -> {
if (followUpToggle.isSelected() == true) {
selectionModel.clearAndSelect(file.getId());
new AddTagAction(getController(), getController().getTagsManager().getFollowUpTagName(), selectionModel.getSelected()).handle(actionEvent);
} else {
new DeleteFollowUpTagAction(getController(), file).handle(actionEvent);
}
})
);
}
protected boolean hasFollowUp() {
if (getFileID().isPresent()) {
try {
TagName followUpTagName = getController().getTagsManager().getFollowUpTagName();
TagName followUpTagName = getController().getTagsManager().getFollowUpTagName();
if (getFile().isPresent()) {
return DrawableAttribute.TAGS.getValue(getFile().get()).stream()
.anyMatch(followUpTagName::equals);
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "failed to get follow up tag name ", ex); //NON-NLS
return true;
} else {
return false;
}
} else {
return false;
}
return false;
}
@Override
@ -342,18 +343,14 @@ public abstract class DrawableTileBase extends DrawableUIBase {
@Override
public void handleTagAdded(ContentTagAddedEvent evt) {
getFileID().ifPresent(fileID -> {
try {
final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName();
final ContentTag addedTag = evt.getAddedTag();
if (fileID == addedTag.getContent().getId()
&& addedTag.getName().equals(followUpTagName)) {
Platform.runLater(() -> {
followUpImageView.setImage(followUpIcon);
followUpToggle.setSelected(true);
});
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to get followup tag name. Unable to update follow up status for file. ", ex); //NON-NLS
final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName(); //NON-NLS
final ContentTag addedTag = evt.getAddedTag();
if (fileID == addedTag.getContent().getId()
&& addedTag.getName().equals(followUpTagName)) {
Platform.runLater(() -> {
followUpImageView.setImage(followUpIcon);
followUpToggle.setSelected(true);
});
}
});
}
@ -362,15 +359,11 @@ public abstract class DrawableTileBase extends DrawableUIBase {
@Override
public void handleTagDeleted(ContentTagDeletedEvent evt) {
getFileID().ifPresent(fileID -> {
try {
final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName();
final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo();
if (fileID == deletedTagInfo.getContentID()
&& deletedTagInfo.getName().equals(followUpTagName)) {
updateFollowUpIcon();
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to get followup tag name. Unable to update follow up status for file. ", ex); //NON-NLS
final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName(); //NON-NLS
final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo();
if (fileID == deletedTagInfo.getContentID()
&& deletedTagInfo.getName().equals(followUpTagName)) {
updateFollowUpIcon();
}
});
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Copyright 2015-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -25,7 +25,6 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
@ -41,7 +40,6 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.controlsfx.control.action.ActionUtils;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction;
@ -55,10 +53,10 @@ import org.sleuthkit.datamodel.TskCoreException;
"DrawableUIBase.errorLabel.OOMText=Insufficent memory"})
abstract public class DrawableUIBase extends AnchorPane implements DrawableView {
/** The use of SingleThreadExecutor means we can only load a single image at
* a time */
static final Executor exec = Executors.newSingleThreadExecutor();
private static final Logger LOGGER = Logger.getLogger(DrawableUIBase.class.getName());
@FXML
BorderPane imageBorder;
@FXML
@ -96,20 +94,17 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
@Override
synchronized public Optional<DrawableFile> getFile() {
if (fileIDOpt.isPresent()) {
if (fileOpt.isPresent() && fileOpt.get().getId() == fileIDOpt.get()) {
return fileOpt;
} else {
if (!fileOpt.isPresent() || fileOpt.get().getId() != fileIDOpt.get()) {
try {
fileOpt = Optional.ofNullable(getController().getFileFromId(fileIDOpt.get()));
fileOpt = Optional.ofNullable(getController().getFileFromID(fileIDOpt.get()));
} catch (TskCoreException ex) {
Logger.getAnonymousLogger().log(Level.WARNING, "failed to get DrawableFile for obj_id" + fileIDOpt.get(), ex); //NON-NLS
fileOpt = Optional.empty();
}
return fileOpt;
}
} else {
return Optional.empty();
fileOpt = Optional.empty();
}
return fileOpt;
}
protected abstract void setFileHelper(Long newFileID);
@ -126,9 +121,7 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
}
synchronized protected void updateContent() {
if (getFile().isPresent()) {
doReadImageTask(getFile().get());
}
getFile().ifPresent(this::doReadImageTask);
}
synchronized Node doReadImageTask(DrawableFile file) {
@ -138,16 +131,16 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
Platform.runLater(() -> imageBorder.setCenter(progressNode));
//called on fx thread
myTask.setOnSucceeded(succeeded -> {
myTask.setOnSucceeded(succeeded -> { //on fx thread
showImage(file, myTask);
synchronized (DrawableUIBase.this) {
imageTask = null;
}
});
myTask.setOnFailed(failed -> {
myTask.setOnFailed(failed -> { //on fx thread
Throwable exception = myTask.getException();
if (exception instanceof OutOfMemoryError
&& exception.getMessage().contains("Java heap space")) { //NON-NLS
&& exception.getMessage().contains("Java heap space")) { //NON-NLS
showErrorNode(Bundle.DrawableUIBase_errorLabel_OOMText(), file);
} else {
showErrorNode(Bundle.DrawableUIBase_errorLabel_text(), file);
@ -156,7 +149,7 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
imageTask = null;
}
});
myTask.setOnCancelled(cancelled -> {
myTask.setOnCancelled(cancelled -> { //on fx thread
synchronized (DrawableUIBase.this) {
imageTask = null;
}
@ -180,9 +173,12 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
}
/**
* Get a new progress indicator to use as a place holder for the image in
* this view.
*
* @param file the value of file
* @param imageTask the value of imageTask
* @param imageTask The imageTask to get a progress indicator for.
*
* @return The new Node to use as a progress indicator.
*/
Node newProgressIndicator(final Task<?> imageTask) {
ProgressIndicator loadingProgressIndicator = new ProgressIndicator(-1);

View File

@ -1,3 +1,21 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery.gui.drawableviews;
import com.google.common.eventbus.Subscribe;
@ -16,8 +34,8 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
@ -60,9 +78,8 @@ public interface DrawableView {
/**
* update the visual representation of the category of the assigned file.
* Implementations of {@link DrawableView} must register themselves with
* {@link CategoryManager#registerListener(java.lang.Object)} to ahve this
* method invoked
* Implementations of DrawableView } must register themselves with
* CategoryManager.registerListener()} to have this method invoked
*
* @param evt the CategoryChangeEvent to handle
*/
@ -95,6 +112,7 @@ public interface DrawableView {
Logger.getLogger(DrawableView.class.getName()).log(Level.WARNING, "Error looking up hash set hits"); //NON-NLS
return false;
}
}
static Border getCategoryBorder(DhsImageCategory category) {

View File

@ -2,6 +2,7 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.RadioButton?>
@ -11,6 +12,7 @@
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
@ -18,7 +20,7 @@
<?import org.controlsfx.control.GridView?>
<?import org.controlsfx.control.SegmentedButton?>
<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1">
<center>
<GridView fx:id="gridView" BorderPane.alignment="CENTER" />
@ -29,7 +31,7 @@
<HBox alignment="CENTER_LEFT" spacing="5.0" BorderPane.alignment="TOP_LEFT">
<children>
<Label fx:id="bottomLabel" text="Group Viewing History: " />
<Button fx:id="backButton" mnemonicParsing="false" text="back">
<Button fx:id="backButton" mnemonicParsing="false" text="Back">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
@ -38,7 +40,7 @@
</ImageView>
</graphic>
</Button>
<Button fx:id="forwardButton" mnemonicParsing="false" text="forward">
<Button fx:id="forwardButton" mnemonicParsing="false" text="Forward">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
@ -56,18 +58,23 @@
<right>
<HBox alignment="CENTER_RIGHT" spacing="5.0" BorderPane.alignment="TOP_RIGHT">
<children>
<Button fx:id="nextButton" contentDisplay="RIGHT" mnemonicParsing="false" text="next unseen group" BorderPane.alignment="CENTER_RIGHT">
<BorderPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</BorderPane.margin>
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../images/control-double.png" />
</image>
</ImageView>
</graphic>
</Button>
<CheckBox fx:id="seenByOtherExaminersCheckBox" mnemonicParsing="false" text="Don't show groups seen by other examiners" />
<AnchorPane fx:id="nextButtonPane" BorderPane.alignment="CENTER_RIGHT">
<BorderPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</BorderPane.margin>
<children>
<Button fx:id="nextButton" contentDisplay="RIGHT" minWidth="175.0" mnemonicParsing="false" text="All Groups Gave Been Seen">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../images/control-double.png" />
</image>
</ImageView>
</graphic>
</Button>
</children>
</AnchorPane>
</children>
<BorderPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -21,6 +21,10 @@ 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 com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -53,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;
@ -81,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;
@ -90,8 +97,8 @@ import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.util.Duration;
import javax.swing.Action;
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;
@ -107,6 +114,7 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.ContextMenuActionsProvider;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.directorytree.ExtractAction;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel;
@ -122,18 +130,17 @@ import org.sleuthkit.autopsy.imagegallery.actions.RedoAction;
import org.sleuthkit.autopsy.imagegallery.actions.SwingMenuItemAdapter;
import org.sleuthkit.autopsy.imagegallery.actions.TagSelectedFilesAction;
import org.sleuthkit.autopsy.imagegallery.actions.UndoAction;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
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 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
@ -144,39 +151,34 @@ import org.sleuthkit.datamodel.TskCoreException;
* https://bitbucket.org/controlsfx/controlsfx/issue/4/add-a-multipleselectionmodel-to-gridview
*/
public class GroupPane extends BorderPane {
private static final Logger LOGGER = Logger.getLogger(GroupPane.class.getName());
private static final Logger logger = Logger.getLogger(GroupPane.class.getName());
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<KeyCode> 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;
private static final List<KeyCode> categoryKeyCodes = Arrays.asList(
NUMPAD0, NUMPAD1, NUMPAD2, NUMPAD3, NUMPAD4, NUMPAD5,
DIGIT0, DIGIT1, DIGIT2, DIGIT3, DIGIT4, DIGIT5);
@FXML
private Button undoButton;
@FXML
private Button redoButton;
@FXML
private SplitMenuButton catSelectedSplitMenu;
@FXML
private SplitMenuButton tagSelectedSplitMenu;
@FXML
private ToolBar headerToolBar;
@FXML
private ToggleButton cat0Toggle;
@FXML
@ -189,30 +191,29 @@ public class GroupPane extends BorderPane {
private ToggleButton cat4Toggle;
@FXML
private ToggleButton cat5Toggle;
@FXML
private SegmentedButton segButton;
private SlideShowView slideShowPane;
@FXML
private ToggleButton slideShowToggle;
@FXML
private GridView<Long> gridView;
@FXML
private ToggleButton tileToggle;
private SlideShowView slideShowPane;
@FXML
private GridView<Long> 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
@ -223,36 +224,33 @@ public class GroupPane extends BorderPane {
private Label catContainerLabel;
@FXML
private Label catHeadingLabel;
@FXML
private HBox catSegmentedContainer;
@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;
GroupViewMode getGroupViewMode() {
return groupViewMode.get();
}
private final Back backAction;
private final Forward forwardAction;
private final NextUnseenGroup nextGroupAction;
/**
* the current GroupViewMode of this GroupPane
*/
private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler();
private ContextMenu contextMenu;
/** the current GroupViewMode of this GroupPane */
private final SimpleObjectProperty<GroupViewMode> 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<DrawableGroup> grouping = new ReadOnlyObjectWrapper<>();
/**
@ -263,7 +261,7 @@ public class GroupPane extends BorderPane {
*/
@ThreadConfined(type = ThreadType.JFX)
private final Map<Long, DrawableCell> cellMap = new HashMap<>();
private final InvalidationListener filesSyncListener = (observable) -> {
final String header = getHeaderString();
final List<Long> fileIds = getGroup().getFileIDs();
@ -273,7 +271,7 @@ public class GroupPane extends BorderPane {
groupLabel.setText(header);
});
};
public GroupPane(ImageGalleryController controller) {
this.controller = controller;
this.selectionModel = controller.getSelectionModel();
@ -282,10 +280,14 @@ public class GroupPane extends BorderPane {
forwardAction = new Forward(controller);
undoAction = new UndoAction(controller);
redoAction = new RedoAction(controller);
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);
@ -301,16 +303,16 @@ public class GroupPane extends BorderPane {
} else {
slideShowPane.setFile(slideShowFileID);
}
setCenter(slideShowPane);
slideShowPane.requestFocus();
}
void syncCatToggle(DrawableFile file) {
getToggleForCategory(file.getCategory()).setSelected(true);
}
public void activateTileViewer() {
groupViewMode.set(GroupViewMode.TILE);
tileToggle.setSelected(true);
@ -322,17 +324,19 @@ public class GroupPane extends BorderPane {
slideShowPane = null;
this.scrollToFileID(selectionModel.lastSelectedProperty().get());
}
public DrawableGroup getGroup() {
return grouping.get();
}
private void selectAllFiles() {
selectionModel.clearAndSelectAll(getGroup().getFileIDs());
}
/**
* 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",
@ -343,15 +347,15 @@ public class GroupPane extends BorderPane {
: Bundle.GroupPane_headerString(StringUtils.defaultIfBlank(getGroup().getGroupByValueDislpayName(), DrawableGroup.getBlankGroupName()),
getGroup().getHashSetHitsCount(), getGroup().getSize());
}
ContextMenu getContextMenu() {
return contextMenu;
}
ReadOnlyObjectProperty<DrawableGroup> grouping() {
return grouping.getReadOnlyProperty();
}
private ToggleButton getToggleForCategory(DhsImageCategory category) {
switch (category) {
case ZERO:
@ -383,20 +387,21 @@ 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);
toggleForCategory.setBorder(new Border(new BorderStroke(cat.getColor(), BorderStrokeStyle.SOLID, CORNER_RADII_2, BORDER_WIDTHS_2)));
@ -421,32 +426,41 @@ public class GroupPane extends BorderPane {
gridView.cellHeightProperty().bind(cellSize);
gridView.cellWidthProperty().bind(cellSize);
gridView.setCellFactory((GridView<Long> 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(() -> {
try {
TagSelectedFilesAction followUpSelectedACtion = new TagSelectedFilesAction(controller.getTagsManager().getFollowUpTagName(), controller);
tagSelectedSplitMenu.setText(followUpSelectedACtion.getText());
tagSelectedSplitMenu.setGraphic(followUpSelectedACtion.getGraphic());
tagSelectedSplitMenu.setOnAction(followUpSelectedACtion);
} catch (TskCoreException tskCoreException) {
LOGGER.log(Level.WARNING, "failed to load FollowUpTagName", tskCoreException); //NON-NLS
}
tagSelectedSplitMenu.setText(followUpSelectedAction.getText());
tagSelectedSplitMenu.setGraphic(followUpSelectedAction.getGraphic());
tagSelectedSplitMenu.setOnAction(followUpSelectedAction);
tagSelectedSplitMenu.showingProperty().addListener(showing -> {
if (tagSelectedSplitMenu.isShowing()) {
List<MenuItem> selTagMenues = Lists.transform(controller.getTagsManager().getNonCategoryTagNames(),
tagName -> GuiUtils.createAutoAssigningMenuItem(tagSelectedSplitMenu, new TagSelectedFilesAction(tagName, controller)));
tagSelectedSplitMenu.getItems().setAll(selTagMenues);
ListenableFuture<List<MenuItem>> getTagsFuture = exec.submit(()
-> Lists.transform(controller.getTagsManager().getNonCategoryTagNames(),
tagName -> GuiUtils.createAutoAssigningMenuItem(tagSelectedSplitMenu, new TagSelectedFilesAction(tagName, controller))));
Futures.addCallback(getTagsFuture, new FutureCallback<List<MenuItem>>() {
@Override
public void onSuccess(List<MenuItem> result) {
tagSelectedSplitMenu.getItems().setAll(result);
}
@Override
public void onFailure(Throwable throwable) {
logger.log(Level.SEVERE, "Error getting tag names.", throwable);
}
}, Platform::runLater);
}
});
});
CategorizeSelectedFilesAction cat5SelectedAction = new CategorizeSelectedFilesAction(DhsImageCategory.FIVE, controller);
catSelectedSplitMenu.setOnAction(cat5SelectedAction);
catSelectedSplitMenu.setText(cat5SelectedAction.getText());
catSelectedSplitMenu.setGraphic(cat5SelectedAction.getGraphic());
catSelectedSplitMenu.showingProperty().addListener(showing -> {
@ -456,12 +470,12 @@ public class GroupPane extends BorderPane {
catSelectedSplitMenu.getItems().setAll(categoryMenues);
}
});
slideShowToggle.getStyleClass().remove("radio-button");
slideShowToggle.getStyleClass().add("toggle-button");
tileToggle.getStyleClass().remove("radio-button");
tileToggle.getStyleClass().add("toggle-button");
bottomLabel.setText(Bundle.GroupPane_bottomLabel_displayText());
headerLabel.setText(Bundle.GroupPane_hederLabel_displayText());
catContainerLabel.setText(Bundle.GroupPane_catContainerLabel_displayText());
@ -481,12 +495,12 @@ public class GroupPane extends BorderPane {
//listen to toggles and update view state
slideShowToggle.setOnAction(onAction -> activateSlideShowViewer(selectionModel.lastSelectedProperty().get()));
tileToggle.setOnAction(onAction -> activateTileViewer());
controller.viewState().addListener((observable, oldViewState, newViewState) -> setViewState(newViewState));
controller.viewStateProperty().addListener((observable, oldViewState, newViewState) -> setViewState(newViewState));
addEventFilter(KeyEvent.KEY_PRESSED, tileKeyboardNavigationHandler);
gridView.addEventHandler(MouseEvent.MOUSE_CLICKED, new MouseHandler());
ActionUtils.configureButton(undoAction, undoButton);
ActionUtils.configureButton(redoAction, redoButton);
ActionUtils.configureButton(forwardAction, forwardButton);
@ -502,7 +516,7 @@ public class GroupPane extends BorderPane {
nextButton.setEffect(null);
onAction.handle(actionEvent);
});
nextGroupAction.disabledProperty().addListener((Observable observable) -> {
boolean newValue = nextGroupAction.isDisabled();
nextButton.setEffect(newValue ? null : DROP_SHADOW);
@ -513,17 +527,27 @@ 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
&& slideShowPane != null) {
&& slideShowPane != null) {
slideShowPane.setFile(newFileId);
} else {
scrollToFileID(newFileId);
}
});
setViewState(controller.viewState().get());
setViewState(controller.viewStateProperty().get());
}
//TODO: make sure we are testing complete visability not just bounds intersection
@ -532,16 +556,16 @@ public class GroupPane extends BorderPane {
if (newFileID == null) {
return; //scrolling to no file doesn't make sense, so abort.
}
final ObservableList<Long> fileIds = gridView.getItems();
int selectedIndex = fileIds.indexOf(newFileID);
if (selectedIndex == -1) {
//somehow we got passed a file id that isn't in the curent group.
//this should never happen, but if it does everything is going to fail, so abort.
return;
}
getScrollBar().ifPresent(scrollBar -> {
DrawableCell cell = cellMap.get(newFileID);
@ -568,14 +592,14 @@ public class GroupPane extends BorderPane {
}
cell = cellMap.get(newFileID);
}
final Bounds gridViewBounds = gridView.localToScene(gridView.getBoundsInLocal());
Bounds tileBounds = cell.localToScene(cell.getBoundsInLocal());
//while the cell is not within the visisble bounds of the gridview, scroll based on screen coordinates
int i = 0;
while (gridViewBounds.contains(tileBounds) == false && (i++ < 100)) {
if (tileBounds.getMinY() < gridViewBounds.getMinY()) {
scrollBar.decrement();
} else if (tileBounds.getMaxY() > gridViewBounds.getMaxY()) {
@ -590,16 +614,16 @@ 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 viewState) {
if (isNull(viewState) || isNull(viewState.getGroup())) {
void setViewState(GroupViewState newViewState) {
if (isNull(newViewState) || isNull(newViewState.getGroup().orElse(null))) {
if (nonNull(getGroup())) {
getGroup().getFileIDs().removeListener(filesSyncListener);
}
this.grouping.set(null);
Platform.runLater(() -> {
gridView.getItems().setAll(Collections.emptyList());
setCenter(null);
@ -611,40 +635,37 @@ public class GroupPane extends BorderPane {
cellMap.clear();
}
});
} else {
if (getGroup() != viewState.getGroup()) {
if (nonNull(getGroup())) {
getGroup().getFileIDs().removeListener(filesSyncListener);
}
this.grouping.set(viewState.getGroup());
getGroup().getFileIDs().addListener(filesSyncListener);
final String header = getHeaderString();
Platform.runLater(() -> {
gridView.getItems().setAll(getGroup().getFileIDs());
slideShowToggle.setDisable(gridView.getItems().isEmpty());
groupLabel.setText(header);
resetScrollBar();
if (viewState.getMode() == GroupViewMode.TILE) {
activateTileViewer();
} else {
activateSlideShowViewer(viewState.getSlideShowfileID().orElse(null));
}
});
if (nonNull(getGroup()) && getGroup() != newViewState.getGroup().get()) {
getGroup().getFileIDs().removeListener(filesSyncListener);
}
this.grouping.set(newViewState.getGroup().get());
getGroup().getFileIDs().addListener(filesSyncListener);
final String header = getHeaderString();
Platform.runLater(() -> {
gridView.getItems().setAll(getGroup().getFileIDs());
slideShowToggle.setDisable(gridView.getItems().isEmpty());
groupLabel.setText(header);
resetScrollBar();
if (newViewState.getMode() == GroupViewMode.TILE) {
activateTileViewer();
} else {
activateSlideShowViewer(newViewState.getSlideShowfileID().orElse(null));
}
});
}
}
@ThreadConfined(type = ThreadType.JFX)
private void resetScrollBar() {
getScrollBar().ifPresent((scrollBar) -> {
scrollBar.setValue(0);
});
getScrollBar().ifPresent(scrollBar -> scrollBar.setValue(0));
}
@ThreadConfined(type = ThreadType.JFX)
private Optional<ScrollBar> getScrollBar() {
if (gridView == null || gridView.getSkin() == null) {
@ -652,28 +673,29 @@ public class GroupPane extends BorderPane {
}
return Optional.ofNullable((ScrollBar) gridView.getSkin().getNode().lookup(".scroll-bar")); //NON-NLS
}
void makeSelection(Boolean shiftDown, Long newFileID) {
if (shiftDown) {
//TODO: do more hear to implement slicker multiselect
int endIndex = grouping.get().getFileIDs().indexOf(newFileID);
int startIndex = IntStream.of(grouping.get().getFileIDs().size(), selectionAnchorIndex, endIndex).min().getAsInt();
endIndex = IntStream.of(0, selectionAnchorIndex, endIndex).max().getAsInt();
List<Long> subList = grouping.get().getFileIDs().subList(Math.max(0, startIndex), Math.min(endIndex, grouping.get().getFileIDs().size()) + 1);
selectionModel.clearAndSelectAll(subList.toArray(new Long[subList.size()]));
selectionModel.select(newFileID);
} else {
selectionAnchorIndex = null;
selectionModel.clearAndSelect(newFileID);
}
}
private class DrawableCell extends GridCell<Long> {
private final DrawableTile tile = new DrawableTile(GroupPane.this, controller);
DrawableCell() {
itemProperty().addListener((ObservableValue<? extends Long> observable, Long oldValue, Long newValue) -> {
if (oldValue != null) {
@ -689,19 +711,18 @@ public class GroupPane extends BorderPane {
}
}
cellMap.put(newValue, DrawableCell.this);
}
});
setGraphic(tile);
}
@Override
protected void updateItem(Long item, boolean empty) {
super.updateItem(item, empty);
tile.setFile(item);
}
void resetItem() {
tile.setFile(null);
}
@ -712,10 +733,10 @@ public class GroupPane extends BorderPane {
* arrows)
*/
private class KeyboardHandler implements EventHandler<KeyEvent> {
@Override
public void handle(KeyEvent t) {
if (t.getEventType() == KeyEvent.KEY_PRESSED) {
switch (t.getCode()) {
case SHIFT:
@ -758,7 +779,7 @@ public class GroupPane extends BorderPane {
t.consume();
break;
}
if (groupViewMode.get() == GroupViewMode.TILE && categoryKeyCodes.contains(t.getCode()) && t.isAltDown()) {
selectAllFiles();
t.consume();
@ -772,7 +793,7 @@ public class GroupPane extends BorderPane {
}
}
}
private DhsImageCategory keyCodeToCat(KeyCode t) {
if (t != null) {
switch (t) {
@ -798,16 +819,16 @@ public class GroupPane extends BorderPane {
}
return null;
}
private void handleArrows(KeyEvent t) {
Long lastSelectFileId = selectionModel.lastSelectedProperty().get();
int lastSelectedIndex = lastSelectFileId != null
? grouping.get().getFileIDs().indexOf(lastSelectFileId)
: Optional.ofNullable(selectionAnchorIndex).orElse(0);
final int columns = Math.max((int) Math.floor((gridView.getWidth() - 18) / (gridView.getCellWidth() + gridView.getHorizontalCellSpacing() * 2)), 1);
final Map<KeyCode, Integer> tileIndexMap = ImmutableMap.of(UP, -columns, DOWN, columns, LEFT, -1, RIGHT, 1);
// implement proper keyboard based multiselect
@ -826,41 +847,44 @@ public class GroupPane extends BorderPane {
}
}
}
private class MouseHandler implements EventHandler<MouseEvent> {
private ContextMenu buildContextMenu() {
ArrayList<MenuItem> menuItems = new ArrayList<>();
menuItems.add(CategorizeAction.getCategoriesMenu(controller));
menuItems.add(AddTagAction.getTagMenu(controller));
Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class);
for (ContextMenuActionsProvider provider : menuProviders) {
for (final Action act : provider.getActions()) {
if (act instanceof Presenter.Popup) {
Presenter.Popup aact = (Presenter.Popup) act;
menuItems.add(SwingMenuItemAdapter.create(aact.getPopupPresenter()));
}
}
try {
menuItems.add(AddTagAction.getTagMenu(controller));
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error building tagging context menu.", ex);
}
Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class).stream()
.map(ContextMenuActionsProvider::getActions)
.flatMap(Collection::stream)
.filter(Presenter.Popup.class::isInstance)
.map(Presenter.Popup.class::cast)
.map(Presenter.Popup::getPopupPresenter)
.map(SwingMenuItemAdapter::create)
.forEachOrdered(menuItems::add);
final MenuItem extractMenuItem = new MenuItem(Bundle.GroupPane_gridViewContextMenuItem_extractFiles());
extractMenuItem.setOnAction((ActionEvent t) -> {
extractMenuItem.setOnAction(actionEvent -> {
SwingUtilities.invokeLater(() -> {
TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID);
ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null));
});
});
menuItems.add(extractMenuItem);
ContextMenu contextMenu = new ContextMenu(menuItems.toArray(new MenuItem[]{}));
contextMenu.setAutoHide(true);
contextMenu.setAutoHide(
true);
return contextMenu;
}
@Override
public void handle(MouseEvent t) {
switch (t.getButton()) {
@ -877,11 +901,11 @@ public class GroupPane extends BorderPane {
if (t.getClickCount() == 1) {
selectAllFiles();
}
if (selectionModel.getSelected().isEmpty() == false) {
if (isNotEmpty(selectionModel.getSelected())) {
if (contextMenu == null) {
contextMenu = buildContextMenu();
}
contextMenu.hide();
contextMenu.show(GroupPane.this, t.getScreenX(), t.getScreenY());
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-15 Basis Technology Corp.
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -55,9 +55,9 @@ import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
@ -67,13 +67,13 @@ import org.sleuthkit.datamodel.TagName;
* Shows details of the selected file.
*/
@NbBundle.Messages({"MetaDataPane.tableView.placeholder=Select a file to show its details here.",
"MetaDataPane.copyMenuItem.text=Copy",
"MetaDataPane.titledPane.displayName=Details",
"MetaDataPane.attributeColumn.headingName=Attribute",
"MetaDataPane.valueColumn.headingName=Value"})
"MetaDataPane.copyMenuItem.text=Copy",
"MetaDataPane.titledPane.displayName=Details",
"MetaDataPane.attributeColumn.headingName=Attribute",
"MetaDataPane.valueColumn.headingName=Value"})
public class MetaDataPane extends DrawableUIBase {
private static final Logger LOGGER = Logger.getLogger(MetaDataPane.class.getName());
private static final Logger logger = Logger.getLogger(MetaDataPane.class.getName());
private static final KeyCodeCombination COPY_KEY_COMBINATION = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_DOWN);
@ -202,7 +202,7 @@ public class MetaDataPane extends DrawableUIBase {
@Override
Task<Image> newReadImageTask(DrawableFile file) {
return file.getThumbnailTask();
return getController().getThumbsCache().getThumbnailTask(file);
}
public void updateAttributesTable() {

View File

@ -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

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2016 Basis Technology Corp.
* Copyright 2016-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -38,6 +38,7 @@ 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;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
/**
* Shows path based groups as a tree and others kinds of groups as a flat list (
@ -77,17 +78,22 @@ final public class GroupTree extends NavPanel<TreeItem<GroupTreeNode>> {
groupTree.setShowRoot(false);
getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change<? extends DrawableGroup> change) -> {
GroupViewState oldState = getController().getViewState();
while (change.next()) {
change.getAddedSubList().stream().forEach(this::insertGroup);
change.getRemoved().stream().forEach(this::removeFromTree);
}
sortGroups();
Platform.runLater(() -> {
GroupTree.this.sortGroups(false);
Optional.ofNullable(oldState)
.flatMap(GroupViewState::getGroup)
.ifPresent(this::setFocusedGroup);
});
});
for (DrawableGroup g : getGroupManager().getAnalyzedGroups()) {
insertGroup(g);
}
sortGroups();
getGroupManager().getAnalyzedGroups().forEach(this::insertGroup);
Platform.runLater(this::sortGroups);
}
/**
@ -102,12 +108,10 @@ final public class GroupTree extends NavPanel<TreeItem<GroupTreeNode>> {
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
}
});
int row = groupTree.getRow(treeItemForGroup);
if (row != -1) {
groupTree.scrollTo(row - 2); //put newly selected row 3 from the top
}
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -18,7 +18,6 @@
*/
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@ -35,8 +34,8 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
/**
* A node in the nav/hash tree. Manages inserts and removals. Has parents and
* children. Does not have graphical properties these are configured in
* {@link GroupTreeCell}. Each GroupTreeItem has a TreeNode which has a path
* segment and may or may not have a group
* GroupTreeCell. Each GroupTreeItem has a TreeNode which has a path segment and
* may or may not have a group
*/
class GroupTreeItem extends TreeItem<GroupTreeNode> {
@ -131,7 +130,6 @@ class GroupTreeItem extends TreeItem<GroupTreeNode> {
}
synchronized GroupTreeItem getTreeItemForPath(List<String> path) {
if (path.isEmpty()) {
// end of recursion
return this;
@ -154,9 +152,7 @@ class GroupTreeItem extends TreeItem<GroupTreeNode> {
if (parent != null) {
parent.childMap.remove(getValue().getPath());
Platform.runLater(() -> {
parent.getChildren().removeAll(Collections.singleton(GroupTreeItem.this));
});
Platform.runLater(() -> parent.getChildren().remove(this));
if (parent.childMap.isEmpty()) {
parent.removeFromParent();
@ -173,8 +169,6 @@ class GroupTreeItem extends TreeItem<GroupTreeNode> {
synchronized void resortChildren(Comparator<DrawableGroup> newComp) {
this.comp = newComp;
getChildren().sort(Comparator.comparing(treeItem -> treeItem.getValue().getGroup(), Comparator.nullsLast(comp)));
for (GroupTreeItem ti : childMap.values()) {
ti.resortChildren(comp);
}
childMap.values().forEach(treeItem -> treeItem.resortChildren(comp));
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2016 Basis Technology Corp.
* Copyright 2016-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -22,6 +22,7 @@ import com.google.common.eventbus.Subscribe;
import java.util.Comparator;
import java.util.Optional;
import java.util.function.Function;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.SelectionModel;
@ -74,9 +75,9 @@ abstract class NavPanel<X> extends Tab {
sortChooser = new SortChooser<>(GroupComparators.getValues());
sortChooser.setComparator(getDefaultComparator());
sortChooser.sortOrderProperty().addListener(order -> sortGroups());
sortChooser.sortOrderProperty().addListener(order -> NavPanel.this.sortGroups());
sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> {
sortGroups();
NavPanel.this.sortGroups();
//only need to listen to changes in category if we are sorting by/ showing the uncategorized count
if (newComparator == GroupComparators.UNCATEGORIZED_COUNT) {
categoryManager.registerListener(NavPanel.this);
@ -90,13 +91,20 @@ abstract class NavPanel<X> extends Tab {
toolBar.getItems().add(sortChooser);
//keep selection in sync with controller
controller.viewState().addListener(observable -> {
Optional.ofNullable(controller.viewState().get())
.map(GroupViewState::getGroup)
.ifPresent(this::setFocusedGroup);
controller.viewStateProperty().addListener(observable -> {
Platform.runLater(()
-> Optional.ofNullable(controller.getViewState())
.flatMap(GroupViewState::getGroup)
.ifPresent(this::setFocusedGroup));
});
getSelectionModel().selectedItemProperty().addListener(o -> updateControllersGroup());
// notify controller about group selection in this view
getSelectionModel().selectedItemProperty()
.addListener((observable, oldItem, newSelectedItem) -> {
Optional.ofNullable(newSelectedItem)
.map(getDataItemMapper())
.ifPresent(group -> controller.advance(GroupViewState.tile(group)));
});
}
/**
@ -106,7 +114,7 @@ abstract class NavPanel<X> extends Tab {
@Subscribe
public void handleCategoryChange(CategoryManager.CategoryChangeEvent event) {
sortGroups();
Platform.runLater(this::sortGroups);
}
/**
@ -121,27 +129,24 @@ abstract class NavPanel<X> extends Tab {
: 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));
}
/**
* Sort the groups in this view according to the currently selected sorting
* options. Attempts to maintain selection.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
void sortGroups() {
sortGroups(true);
}
public void sortGroups(boolean preserveSelection) {
X selectedItem = getSelectionModel().getSelectedItem();
applyGroupComparator();
Optional.ofNullable(selectedItem)
.map(getDataItemMapper())
.ifPresent(this::setFocusedGroup);
if (preserveSelection) {
Optional.ofNullable(selectedItem)
.map(getDataItemMapper())
.ifPresent(this::setFocusedGroup);
}
}
/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -18,13 +18,20 @@
*/
package org.sleuthkit.autopsy.imagegallery.utils;
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 javafx.concurrent.Task;
/**
*
*/
public class TaskUtils {
public final class TaskUtils {
private TaskUtils() {
}
public static <T> Task<T> taskFrom(Callable<T> callable) {
return new Task<T>() {
@ -35,6 +42,8 @@ public class TaskUtils {
};
}
private TaskUtils() {
public static ListeningExecutorService getExecutorForClass(Class<?> clazz) {
return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder().setNameFormat("Image Gallery " + clazz.getSimpleName() + " BG Thread").build()));
}
}

View File

@ -1,5 +1,5 @@
#Updated by build script
#Mon, 25 Jun 2018 17:19:36 -0400
#Mon, 03 Sep 2018 17:29:44 +0200
LBL_splash_window_title=Starting Autopsy
SPLASH_HEIGHT=314
SPLASH_WIDTH=538

View File

@ -1,4 +1,4 @@
#Updated by build script
#Mon, 25 Jun 2018 17:19:36 -0400
#Mon, 03 Sep 2018 17:29:44 +0200
CTL_MainWindow_Title=Autopsy 4.8.0
CTL_MainWindow_Title_No_Project=Autopsy 4.8.0