mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-15 01:07:42 +00:00
Merge remote-tracking branch 'refs/remotes/sleuthkit/develop' into develop
This commit is contained in:
commit
a2312c92e0
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2011 Basis Technology Corp.
|
||||
* Copyright 2011-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -95,16 +95,18 @@ class ThumbnailViewNode extends FilterNode {
|
||||
super.done();
|
||||
try {
|
||||
iconCache = new SoftReference<>(super.get());
|
||||
progressHandle.finish();
|
||||
fireIconChange();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
Logger.getLogger(ThumbnailViewNode.class.getName()).log(Level.SEVERE, "Error getting thumbnail icon", ex); //NON-NLS
|
||||
} finally {
|
||||
progressHandle.finish();
|
||||
if (timer != null) {
|
||||
timer.stop();
|
||||
timer = null;
|
||||
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
Logger.getLogger(ThumbnailViewNode.class.getName()).log(Level.SEVERE, "Error getting thumbnail icon", ex); //NON-NLS
|
||||
swingWorker = null;
|
||||
}
|
||||
swingWorker = null;
|
||||
}
|
||||
};
|
||||
swingWorker.execute();
|
||||
|
@ -21,7 +21,3 @@ PlatformUtil.getAllMemUsageInfo.usageText={0}\n\
|
||||
{1}\n\
|
||||
Process Virtual Memory\: {2}
|
||||
StringExtract.illegalStateException.cannotInit.msg=Unicode table not properly initialized, cannot instantiate StringExtract
|
||||
ImageUtils.GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}
|
||||
ImageUtils.GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}
|
||||
ImageUtils.ReadImageTask.mesage.text=Reading image\: {0}
|
||||
VideoUtils.genVideoThumb.progress.text=extracting temporary file {0}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2012-15 Basis Technology Corp.
|
||||
* Copyright 2012-16 Basis Technology Corp.
|
||||
*
|
||||
* Copyright 2012 42six Solutions.
|
||||
* Contact: aebadirad <at> 42six <dot> com
|
||||
@ -30,6 +30,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
@ -74,10 +75,6 @@ public class ImageUtils {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ImageUtils.class.getName());
|
||||
|
||||
private static final String COULD_NOT_WRITE_CACHE_THUMBNAIL = "Could not write cache thumbnail: "; //NOI18N NON-NLS
|
||||
private static final String COULD_NOT_CREATE_IMAGE_INPUT_STREAM = "Could not create ImageInputStream."; //NOI18N NON-NLS
|
||||
private static final String NO_IMAGE_READER_FOUND_FOR_ = "No ImageReader found for "; //NOI18N NON-NLS
|
||||
|
||||
/**
|
||||
* save thumbnails to disk as this format
|
||||
*/
|
||||
@ -594,7 +591,7 @@ public class ImageUtils {
|
||||
try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) {
|
||||
try (ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
|
||||
if (input == null) {
|
||||
IIOException iioException = new IIOException(COULD_NOT_CREATE_IMAGE_INPUT_STREAM);
|
||||
IIOException iioException = new IIOException("Could not create ImageInputStream.");
|
||||
LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
|
||||
throw iioException;
|
||||
}
|
||||
@ -613,7 +610,7 @@ public class ImageUtils {
|
||||
reader.dispose();
|
||||
}
|
||||
} else {
|
||||
IIOException iioException = new IIOException(NO_IMAGE_READER_FOUND_FOR_ + getContentPathSafe(file));
|
||||
IIOException iioException = new IIOException("No ImageReader found.");
|
||||
LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
|
||||
|
||||
throw iioException;
|
||||
@ -646,18 +643,19 @@ public class ImageUtils {
|
||||
*/
|
||||
static private class GetThumbnailTask extends ReadImageTaskBase {
|
||||
|
||||
private static final String FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION = "Failed to read image for thumbnail generation."; //NOI18N NON-NLS
|
||||
private static final String FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION = "Failed to read {0} for thumbnail generation."; //NOI18N NON-NLS
|
||||
|
||||
private final int iconSize;
|
||||
private final File cacheFile;
|
||||
private final boolean defaultOnFailure;
|
||||
|
||||
// @NbBundle.Messages({"# {0} - file name",
|
||||
// "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}", "# {0} - file name",
|
||||
// "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
|
||||
@NbBundle.Messages({"# {0} - file name",
|
||||
"GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}",
|
||||
"# {0} - file name",
|
||||
"GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
|
||||
private GetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) {
|
||||
super(file);
|
||||
updateMessage(NbBundle.getMessage(this.getClass(), "ImageUtils.GetOrGenerateThumbnailTask.loadingThumbnailFor", file.getName()));
|
||||
updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName()));
|
||||
this.iconSize = iconSize;
|
||||
this.defaultOnFailure = defaultOnFailure;
|
||||
this.cacheFile = getCachedThumbnailLocation(file.getId());
|
||||
@ -678,36 +676,39 @@ public class ImageUtils {
|
||||
if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) {
|
||||
return SwingFXUtils.toFXImage(cachedThumbnail, null);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
LOGGER.log(Level.WARNING, "ImageIO had a problem reading thumbnail for image {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS
|
||||
} catch (Exception ex) {
|
||||
LOGGER.log(Level.WARNING, "ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS
|
||||
cacheFile.delete(); //since we can't read the file we might as well delete it.
|
||||
}
|
||||
}
|
||||
|
||||
if (isCancelled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//There was no correctly-sized cached thumbnail so make one.
|
||||
BufferedImage thumbnail = null;
|
||||
|
||||
if (VideoUtils.isVideoThumbnailSupported(file)) {
|
||||
if (openCVLoaded) {
|
||||
updateMessage(NbBundle.getMessage(this.getClass(), "ImageUtils.GetOrGenerateThumbnailTask.generatingPreviewFor", file.getName()));
|
||||
updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
|
||||
thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize);
|
||||
}
|
||||
if (null == thumbnail) {
|
||||
if (defaultOnFailure) {
|
||||
thumbnail = DEFAULT_THUMBNAIL;
|
||||
} else {
|
||||
throw new IIOException("Failed to generate thumbnail for video file.");
|
||||
throw new IIOException("Failed to generate a thumbnail for " + getContentPathSafe(file));
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
//read the image into a buffered image.
|
||||
//TODO: I don't like this, we just converted it from BufferedIamge to fx Image -jm
|
||||
BufferedImage bufferedImage = SwingFXUtils.fromFXImage(readImage(), null);
|
||||
if (null == bufferedImage) {
|
||||
LOGGER.log(Level.WARNING, FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION);
|
||||
throw new IIOException(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION);
|
||||
String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file));
|
||||
LOGGER.log(Level.WARNING, msg);
|
||||
throw new IIOException(msg);
|
||||
}
|
||||
updateProgress(-1, 1);
|
||||
|
||||
@ -716,23 +717,21 @@ public class ImageUtils {
|
||||
thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize);
|
||||
} catch (IllegalArgumentException | OutOfMemoryError e) {
|
||||
// if resizing does not work due to extreme aspect ratio or oom, crop the image instead.
|
||||
LOGGER.log(Level.WARNING, "Could not scale image {0}: " + e.toString() + ". Attemptying to crop {0} instead", ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS
|
||||
LOGGER.log(Level.WARNING, "Cropping {0}, because it could not be scaled: " + e.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS
|
||||
|
||||
final int height = bufferedImage.getHeight();
|
||||
final int width = bufferedImage.getWidth();
|
||||
if (iconSize < height || iconSize < width) {
|
||||
final int cropHeight = Math.min(iconSize, height);
|
||||
final int cropWidth = Math.min(iconSize, width);
|
||||
|
||||
try {
|
||||
thumbnail = ScalrWrapper.cropImage(bufferedImage, cropWidth, cropHeight);
|
||||
} catch (Exception cropException) {
|
||||
LOGGER.log(Level.WARNING, "Could not crop image {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS
|
||||
throw cropException;
|
||||
LOGGER.log(Level.WARNING, "Could not crop {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.log(Level.WARNING, "Could not scale image {0}: " + e.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS
|
||||
LOGGER.log(Level.WARNING, "Could not scale {0}: " + e.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@ -744,7 +743,7 @@ public class ImageUtils {
|
||||
updateProgress(-1, 1);
|
||||
|
||||
//if we got a valid thumbnail save it
|
||||
if ((cacheFile != null) && nonNull(thumbnail) && DEFAULT_THUMBNAIL != thumbnail) {
|
||||
if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) {
|
||||
saveThumbnail(thumbnail);
|
||||
}
|
||||
|
||||
@ -790,16 +789,16 @@ public class ImageUtils {
|
||||
/**
|
||||
* A task that reads the content of a AbstractFile as a javafx Image.
|
||||
*/
|
||||
@NbBundle.Messages({
|
||||
"# {0} - file name",
|
||||
"ReadImageTask.mesageText=Reading image: {0}"})
|
||||
static private class ReadImageTask extends ReadImageTaskBase {
|
||||
|
||||
ReadImageTask(AbstractFile file) {
|
||||
super(file);
|
||||
updateMessage(NbBundle.getMessage(this.getClass(), "ImageUtils.ReadImageTask.mesage.text", file.getName()));
|
||||
updateMessage(Bundle.ReadImageTask_mesageText(file.getName()));
|
||||
}
|
||||
|
||||
// @NbBundle.Messages({
|
||||
// "# {0} - file name",
|
||||
// "LoadImageTask.mesageText=Reading image: {0}"})
|
||||
@Override
|
||||
protected javafx.scene.image.Image call() throws Exception {
|
||||
return readImage();
|
||||
@ -811,70 +810,55 @@ public class ImageUtils {
|
||||
*/
|
||||
static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener {
|
||||
|
||||
private static final String IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT = "ImageUtils could not read {0}. It may be unsupported or corrupt"; //NOI18N NON-NLS
|
||||
private static final String IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT = "ImageIO could not read {0}. It may be unsupported or corrupt"; //NOI18N NON-NLS
|
||||
final AbstractFile file;
|
||||
private ImageReader reader;
|
||||
// private ImageReader reader;
|
||||
|
||||
ReadImageTaskBase(AbstractFile file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
protected javafx.scene.image.Image readImage() throws IOException {
|
||||
try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) {
|
||||
if (ImageUtils.isGIF(file)) {
|
||||
//use JavaFX to directly read GIF to preserve potential animation,
|
||||
javafx.scene.image.Image image = new javafx.scene.image.Image(new BufferedInputStream(inputStream));
|
||||
if (image.isError() == false) {
|
||||
return image;
|
||||
}
|
||||
//fall through to default image reading code if there was an error
|
||||
if (ImageUtils.isGIF(file)) {
|
||||
//use JavaFX to directly read GIF to preserve potential animation
|
||||
javafx.scene.image.Image image = new javafx.scene.image.Image(new BufferedInputStream(new ReadContentInputStream(file)));
|
||||
if (image.isError() == false) {
|
||||
return image;
|
||||
}
|
||||
if (isCancelled()) {
|
||||
return null;
|
||||
}
|
||||
try (ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
|
||||
if (input == null) {
|
||||
throw new IIOException(COULD_NOT_CREATE_IMAGE_INPUT_STREAM);
|
||||
}
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
|
||||
//fall through to default image reading code if there was an error
|
||||
}
|
||||
if (isCancelled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//we use the first ImageReader, is there any point to trying the others?
|
||||
if (readers.hasNext()) {
|
||||
reader = readers.next();
|
||||
reader.addIIOReadProgressListener(this);
|
||||
reader.setInput(input);
|
||||
return getImageProperty(file, "ImageIO could not read {0}: ",
|
||||
imageReader -> {
|
||||
imageReader.addIIOReadProgressListener(ReadImageTaskBase.this);
|
||||
/*
|
||||
* This is the important part, get or create a
|
||||
* ImageReadParam, create a destination image to hold
|
||||
* the decoded result, then pass that image with the
|
||||
* param.
|
||||
*/
|
||||
ImageReadParam param = reader.getDefaultReadParam();
|
||||
|
||||
BufferedImage bufferedImage = reader.getImageTypes(0).next().createBufferedImage(reader.getWidth(0), reader.getHeight(0));
|
||||
ImageReadParam param = imageReader.getDefaultReadParam();
|
||||
BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
|
||||
param.setDestination(bufferedImage);
|
||||
try {
|
||||
bufferedImage = reader.read(0, param); //should always be same bufferedImage object
|
||||
bufferedImage = imageReader.read(0, param); //should always be same bufferedImage object
|
||||
} catch (IOException iOException) {
|
||||
// Ignore this exception or display a warning or similar, for exceptions happening during decoding
|
||||
LOGGER.log(Level.WARNING, IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + iOException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N
|
||||
LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + iOException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N
|
||||
} finally {
|
||||
reader.removeIIOReadProgressListener(this);
|
||||
reader.dispose();
|
||||
imageReader.removeIIOReadProgressListener(ReadImageTaskBase.this);
|
||||
}
|
||||
if (isCancelled()) {
|
||||
return null;
|
||||
}
|
||||
return SwingFXUtils.toFXImage(bufferedImage, null);
|
||||
} else {
|
||||
throw new IIOException(NO_IMAGE_READER_FOUND_FOR_ + ImageUtils.getContentPathSafe(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void imageProgress(ImageReader source, float percentageDone) {
|
||||
public void imageProgress(ImageReader reader, float percentageDone) {
|
||||
//update this task with the progress reported by ImageReader.read
|
||||
updateProgress(percentageDone, 100);
|
||||
if (isCancelled()) {
|
||||
@ -890,11 +874,11 @@ public class ImageUtils {
|
||||
try {
|
||||
javafx.scene.image.Image fxImage = get();
|
||||
if (fxImage == null) {
|
||||
LOGGER.log(Level.WARNING, IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT, ImageUtils.getContentPathSafe(file));
|
||||
LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT, ImageUtils.getContentPathSafe(file));
|
||||
} else {
|
||||
if (fxImage.isError()) {
|
||||
//if there was somekind of error, log it
|
||||
LOGGER.log(Level.WARNING, IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + ObjectUtils.toString(fxImage.getException()), ImageUtils.getContentPathSafe(file));
|
||||
LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + ObjectUtils.toString(fxImage.getException()), ImageUtils.getContentPathSafe(file));
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
@ -905,7 +889,7 @@ public class ImageUtils {
|
||||
@Override
|
||||
protected void failed() {
|
||||
super.failed();
|
||||
LOGGER.log(Level.WARNING, IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + ObjectUtils.toString(getException()), ImageUtils.getContentPathSafe(file));
|
||||
LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + ObjectUtils.toString(getException()), ImageUtils.getContentPathSafe(file));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -950,7 +934,7 @@ public class ImageUtils {
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static String getContentPathSafe(Content content) {
|
||||
static String getContentPathSafe(Content content) {
|
||||
try {
|
||||
return content.getUniquePath();
|
||||
} catch (TskCoreException tskCoreException) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2015 Basis Technology Corp.
|
||||
* Copyright 2015-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.coreutils;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -91,33 +92,33 @@ public class VideoUtils {
|
||||
return isMediaThumbnailSupported(file, SUPPORTED_VIDEO_MIME_TYPES, SUPPORTED_VIDEO_EXTENSIONS, CONDITIONAL_MIME_TYPES);
|
||||
}
|
||||
|
||||
@NbBundle.Messages({"# {0} - file name",
|
||||
"VideoUtils.genVideoThumb.progress.text=extracting temporary file {0}"})
|
||||
static BufferedImage generateVideoThumbnail(AbstractFile file, int iconSize) {
|
||||
java.io.File tempFile = getTempVideoFile(file);
|
||||
|
||||
try {
|
||||
if (tempFile.exists() == false || tempFile.length() < file.getSize()) {
|
||||
com.google.common.io.Files.createParentDirs(tempFile);
|
||||
ProgressHandle progress = ProgressHandleFactory.createHandle(NbBundle.getMessage(VideoUtils.class, "VideoUtils.genVideoThumb.progress.text", file.getName()));
|
||||
progress.start(100);
|
||||
try {
|
||||
ContentUtils.writeToFile(file, tempFile, progress, null, true);
|
||||
} catch (IOException ex) {
|
||||
LOGGER.log(Level.WARNING, "Error buffering file", ex); //NON-NLS
|
||||
}
|
||||
if (tempFile.exists() == false || tempFile.length() < file.getSize()) {
|
||||
ProgressHandle progress = ProgressHandleFactory.createHandle(Bundle.VideoUtils_genVideoThumb_progress_text(file.getName()));
|
||||
progress.start(100);
|
||||
try {
|
||||
Files.createParentDirs(tempFile);
|
||||
ContentUtils.writeToFile(file, tempFile, progress, null, true);
|
||||
} catch (IOException ex) {
|
||||
LOGGER.log(Level.WARNING, "Error extracting temporary file for " + ImageUtils.getContentPathSafe(file), ex); //NON-NLS
|
||||
} finally {
|
||||
progress.finish();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VideoCapture videoFile = new VideoCapture(); // will contain the video
|
||||
|
||||
if (!videoFile.open(tempFile.toString())) {
|
||||
LOGGER.log(Level.WARNING, "Error opening {0} for preview generation.", ImageUtils.getContentPathSafe(file)); //NON-NLS
|
||||
return null;
|
||||
}
|
||||
double fps = videoFile.get(CV_CAP_PROP_FPS); // gets frame per second
|
||||
double totalFrames = videoFile.get(CV_CAP_PROP_FRAME_COUNT); // gets total frames
|
||||
if (fps <= 0 || totalFrames <= 0) {
|
||||
LOGGER.log(Level.WARNING, "Error getting fps or total frames for {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS
|
||||
return null;
|
||||
}
|
||||
double milliseconds = 1000 * (totalFrames / fps); //total milliseconds
|
||||
@ -132,10 +133,12 @@ public class VideoUtils {
|
||||
for (int x = 0; x < THUMB_COLUMNS; x++) {
|
||||
for (int y = 0; y < THUMB_ROWS; y++) {
|
||||
if (!videoFile.set(CV_CAP_PROP_POS_MSEC, timestamp + x * framkeskip + y * framkeskip * THUMB_COLUMNS)) {
|
||||
LOGGER.log(Level.WARNING, "Error seeking to " + timestamp + "ms in {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS
|
||||
break; // if we can't set the time, return black for that frame
|
||||
}
|
||||
//read the frame into the image/matrix
|
||||
if (!videoFile.read(imageMatrix)) {
|
||||
LOGGER.log(Level.WARNING, "Error reading frames at " + timestamp + "ms from {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS
|
||||
break; //if the image for some reason is bad, return black for that frame
|
||||
}
|
||||
|
||||
|
@ -107,6 +107,8 @@ public enum FileTypeUtils {
|
||||
, "sn", "ras" //sun raster NON-NLS
|
||||
, "ico" //windows icons NON-NLS
|
||||
, "tga" //targa NON-NLS
|
||||
, "wmf", "emf" // windows meta file NON-NLS
|
||||
, "wmz", "emz" //compressed windows meta file NON-NLS
|
||||
));
|
||||
|
||||
//add list of known video extensions
|
||||
@ -129,6 +131,8 @@ public enum FileTypeUtils {
|
||||
* mime types.
|
||||
*/
|
||||
supportedMimeTypes.addAll(Arrays.asList("application/x-123")); //NON-NLS
|
||||
supportedMimeTypes.addAll(Arrays.asList("application/x-wmf")); //NON-NLS
|
||||
supportedMimeTypes.addAll(Arrays.asList("application/x-emf")); //NON-NLS
|
||||
|
||||
//add list of mimetypes ImageIO claims to support
|
||||
supportedMimeTypes.addAll(Stream.of(ImageIO.getReaderMIMETypes())
|
||||
|
@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.imagegallery;
|
||||
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@ -54,7 +55,6 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.SwingUtilities;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.netbeans.api.progress.ProgressHandle;
|
||||
import org.netbeans.api.progress.ProgressHandleFactory;
|
||||
import org.openide.util.Cancellable;
|
||||
@ -85,6 +85,7 @@ import org.sleuthkit.datamodel.Image;
|
||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
import org.sleuthkit.datamodel.TskData;
|
||||
import org.sleuthkit.datamodel.VirtualDirectory;
|
||||
|
||||
/**
|
||||
* Connects different parts of ImageGallery together and is hub for flow of
|
||||
@ -127,8 +128,6 @@ public final class ImageGalleryController implements Executor {
|
||||
*/
|
||||
private final SimpleBooleanProperty listeningEnabled = new SimpleBooleanProperty(false);
|
||||
|
||||
private final ReadOnlyIntegerWrapper queueSizeProperty = new ReadOnlyIntegerWrapper(0);
|
||||
|
||||
private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false);
|
||||
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
@ -153,7 +152,6 @@ public final class ImageGalleryController implements Executor {
|
||||
|
||||
private Node infoOverlay;
|
||||
private SleuthkitCase sleuthKitCase;
|
||||
// private NavPanel navPanel;
|
||||
|
||||
public ReadOnlyBooleanProperty getMetaDataCollapsed() {
|
||||
return metaDataCollapsed.getReadOnlyProperty();
|
||||
@ -175,7 +173,7 @@ public final class ImageGalleryController implements Executor {
|
||||
return historyManager.currentState();
|
||||
}
|
||||
|
||||
public synchronized FileIDSelectionModel getSelectionModel() {
|
||||
public FileIDSelectionModel getSelectionModel() {
|
||||
return selectionModel;
|
||||
}
|
||||
|
||||
@ -187,12 +185,16 @@ public final class ImageGalleryController implements Executor {
|
||||
return db;
|
||||
}
|
||||
|
||||
synchronized public void setListeningEnabled(boolean enabled) {
|
||||
listeningEnabled.set(enabled);
|
||||
public void setListeningEnabled(boolean enabled) {
|
||||
synchronized (listeningEnabled) {
|
||||
listeningEnabled.set(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized boolean isListeningEnabled() {
|
||||
return listeningEnabled.get();
|
||||
boolean isListeningEnabled() {
|
||||
synchronized (listeningEnabled) {
|
||||
return listeningEnabled.get();
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
|
||||
@ -248,12 +250,14 @@ public final class ImageGalleryController implements Executor {
|
||||
checkForGroups();
|
||||
});
|
||||
|
||||
IngestManager.getInstance().addIngestModuleEventListener((PropertyChangeEvent evt) -> {
|
||||
Platform.runLater(this::updateRegroupDisabled);
|
||||
});
|
||||
IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {
|
||||
Platform.runLater(this::updateRegroupDisabled);
|
||||
});
|
||||
IngestManager ingestManager = IngestManager.getInstance();
|
||||
PropertyChangeListener ingestEventHandler =
|
||||
propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled);
|
||||
|
||||
ingestManager.addIngestModuleEventListener(ingestEventHandler);
|
||||
ingestManager.addIngestJobEventListener(ingestEventHandler);
|
||||
|
||||
queueSizeProperty.addListener(obs -> this.updateRegroupDisabled());
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty getCanAdvance() {
|
||||
@ -280,8 +284,9 @@ public final class ImageGalleryController implements Executor {
|
||||
return historyManager.retreat();
|
||||
}
|
||||
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
private void updateRegroupDisabled() {
|
||||
regroupDisabled.set(getFileUpdateQueueSizeProperty().get() > 0 || IngestManager.getInstance().isIngestRunning());
|
||||
regroupDisabled.set((queueSizeProperty.get() > 0) || IngestManager.getInstance().isIngestRunning());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -290,16 +295,16 @@ public final class ImageGalleryController implements Executor {
|
||||
* 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."})
|
||||
@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."})
|
||||
public void checkForGroups() {
|
||||
if (groupManager.getAnalyzedGroups().isEmpty()) {
|
||||
if (IngestManager.getInstance().isIngestRunning()) {
|
||||
@ -312,7 +317,7 @@ public final class ImageGalleryController implements Executor {
|
||||
new ProgressIndicator()));
|
||||
}
|
||||
|
||||
} else if (getFileUpdateQueueSizeProperty().get() > 0) {
|
||||
} else if (queueSizeProperty.get() > 0) {
|
||||
replaceNotification(fullUIStackPane,
|
||||
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(),
|
||||
new ProgressIndicator()));
|
||||
@ -357,20 +362,14 @@ public final class ImageGalleryController implements Executor {
|
||||
}
|
||||
}
|
||||
|
||||
private void restartWorker() {
|
||||
if (dbWorkerThread != null) {
|
||||
synchronized private DBWorkerThread restartWorker() {
|
||||
if (dbWorkerThread == null) {
|
||||
dbWorkerThread = new DBWorkerThread(this);
|
||||
dbWorkerThread.start();
|
||||
} else {
|
||||
// Keep using the same worker thread if one exists
|
||||
return;
|
||||
}
|
||||
dbWorkerThread = new DBWorkerThread();
|
||||
|
||||
getFileUpdateQueueSizeProperty().addListener((Observable o) -> {
|
||||
Platform.runLater(this::updateRegroupDisabled);
|
||||
});
|
||||
|
||||
Thread th = new Thread(dbWorkerThread, "DB-Worker-Thread");
|
||||
th.setDaemon(false); // we want it to go away when it is done
|
||||
th.start();
|
||||
return dbWorkerThread;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -411,15 +410,16 @@ public final class ImageGalleryController implements Executor {
|
||||
setListeningEnabled(false);
|
||||
ThumbnailCache.getDefault().clearCache();
|
||||
historyManager.clear();
|
||||
groupManager.clear();
|
||||
tagsManager.clearFollowUpTagName();
|
||||
tagsManager.unregisterListener(groupManager);
|
||||
tagsManager.unregisterListener(categoryManager);
|
||||
dbWorkerThread.cancelAllTasks();
|
||||
dbWorkerThread.cancel();
|
||||
dbWorkerThread = null;
|
||||
restartWorker();
|
||||
dbWorkerThread = restartWorker();
|
||||
|
||||
Toolbar.getDefault(this).reset();
|
||||
groupManager.clear();
|
||||
|
||||
if (db != null) {
|
||||
db.closeDBCon();
|
||||
}
|
||||
@ -431,11 +431,9 @@ public final class ImageGalleryController implements Executor {
|
||||
*
|
||||
* @param innerTask
|
||||
*/
|
||||
public void queueDBWorkerTask(InnerTask innerTask) {
|
||||
|
||||
// @@@ We could make a lock for the worker thread
|
||||
public synchronized void queueDBWorkerTask(BackgroundTask innerTask) {
|
||||
if (dbWorkerThread == null) {
|
||||
restartWorker();
|
||||
dbWorkerThread = restartWorker();
|
||||
}
|
||||
dbWorkerThread.addTask(innerTask);
|
||||
}
|
||||
@ -455,10 +453,6 @@ public final class ImageGalleryController implements Executor {
|
||||
Platform.runLater(this::checkForGroups);
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty getFileUpdateQueueSizeProperty() {
|
||||
return queueSizeProperty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty regroupProgress() {
|
||||
return groupManager.regroupProgress();
|
||||
}
|
||||
@ -496,29 +490,43 @@ public final class ImageGalleryController implements Executor {
|
||||
return undoManager;
|
||||
}
|
||||
|
||||
// @@@ REVIEW IF THIS SHOLD BE STATIC...
|
||||
//TODO: concept seems like the controller deal with how much work to do at a given time
|
||||
public ReadOnlyIntegerProperty getDBTasksQueueSizeProperty() {
|
||||
return queueSizeProperty.getReadOnlyProperty();
|
||||
}
|
||||
private final ReadOnlyIntegerWrapper queueSizeProperty = new ReadOnlyIntegerWrapper(0);
|
||||
|
||||
// @@@ review this class for synchronization issues (i.e. reset and cancel being called, add, etc.)
|
||||
private class DBWorkerThread implements Runnable {
|
||||
static private class DBWorkerThread extends Thread implements Cancellable {
|
||||
|
||||
private final ImageGalleryController controller;
|
||||
|
||||
DBWorkerThread(ImageGalleryController controller) {
|
||||
super("DB-Worker-Thread");
|
||||
setDaemon(false);
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
// true if the process was requested to stop. Currently no way to reset it
|
||||
private volatile boolean cancelled = false;
|
||||
|
||||
// list of tasks to run
|
||||
private final BlockingQueue<InnerTask> workQueue = new LinkedBlockingQueue<>();
|
||||
private final BlockingQueue<BackgroundTask> workQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
/**
|
||||
* Cancel all of the queued up tasks and the currently scheduled task.
|
||||
* Note that after you cancel, you cannot submit new jobs to this
|
||||
* thread.
|
||||
*/
|
||||
public void cancelAllTasks() {
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
cancelled = true;
|
||||
for (InnerTask it : workQueue) {
|
||||
for (BackgroundTask it : workQueue) {
|
||||
it.cancel();
|
||||
}
|
||||
workQueue.clear();
|
||||
queueSizeProperty.set(workQueue.size());
|
||||
int size = workQueue.size();
|
||||
Platform.runLater(() -> controller.queueSizeProperty.set(size));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -526,11 +534,10 @@ public final class ImageGalleryController implements Executor {
|
||||
*
|
||||
* @param it
|
||||
*/
|
||||
public void addTask(InnerTask it) {
|
||||
public void addTask(BackgroundTask it) {
|
||||
workQueue.add(it);
|
||||
Platform.runLater(() -> {
|
||||
queueSizeProperty.set(workQueue.size());
|
||||
});
|
||||
int size = workQueue.size();
|
||||
Platform.runLater(() -> controller.queueSizeProperty.set(size));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -538,19 +545,17 @@ public final class ImageGalleryController implements Executor {
|
||||
|
||||
// nearly infinite loop waiting for tasks
|
||||
while (true) {
|
||||
if (cancelled) {
|
||||
if (cancelled || isInterrupted()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
InnerTask it = workQueue.take();
|
||||
BackgroundTask it = workQueue.take();
|
||||
|
||||
if (it.isCancelled() == false) {
|
||||
it.run();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
queueSizeProperty.set(workQueue.size());
|
||||
});
|
||||
int size = workQueue.size();
|
||||
Platform.runLater(() -> controller.queueSizeProperty.set(size));
|
||||
|
||||
} catch (InterruptedException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Failed to run DB worker thread", ex); //NON-NLS
|
||||
@ -567,8 +572,15 @@ public final class ImageGalleryController implements Executor {
|
||||
* Abstract base class for task to be done on {@link DBWorkerThread}
|
||||
*/
|
||||
@NbBundle.Messages({"ImageGalleryController.InnerTask.progress.name=progress",
|
||||
"ImageGalleryController.InnerTask.message.name=status"})
|
||||
static public abstract class InnerTask implements Runnable, Cancellable {
|
||||
"ImageGalleryController.InnerTask.message.name=status"})
|
||||
static public abstract class BackgroundTask implements Runnable, Cancellable {
|
||||
|
||||
private final SimpleObjectProperty<Worker.State> state = new SimpleObjectProperty<>(Worker.State.READY);
|
||||
private final SimpleDoubleProperty progress = new SimpleDoubleProperty(this, Bundle.ImageGalleryController_InnerTask_progress_name());
|
||||
private final SimpleStringProperty message = new SimpleStringProperty(this, Bundle.ImageGalleryController_InnerTask_message_name());
|
||||
|
||||
protected BackgroundTask() {
|
||||
}
|
||||
|
||||
public double getProgress() {
|
||||
return progress.get();
|
||||
@ -585,9 +597,6 @@ public final class ImageGalleryController implements Executor {
|
||||
public final void updateMessage(String Status) {
|
||||
this.message.set(Status);
|
||||
}
|
||||
SimpleObjectProperty<Worker.State> state = new SimpleObjectProperty<>(Worker.State.READY);
|
||||
SimpleDoubleProperty progress = new SimpleDoubleProperty(this, Bundle.ImageGalleryController_InnerTask_progress_name());
|
||||
SimpleStringProperty message = new SimpleStringProperty(this, Bundle.ImageGalleryController_InnerTask_message_name());
|
||||
|
||||
public SimpleDoubleProperty progressProperty() {
|
||||
return progress;
|
||||
@ -601,24 +610,21 @@ public final class ImageGalleryController implements Executor {
|
||||
return state.get();
|
||||
}
|
||||
|
||||
protected void updateState(Worker.State newState) {
|
||||
state.set(newState);
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Worker.State> stateProperty() {
|
||||
return new ReadOnlyObjectWrapper<>(state.get());
|
||||
}
|
||||
|
||||
protected InnerTask() {
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized public boolean cancel() {
|
||||
public synchronized boolean cancel() {
|
||||
updateState(Worker.State.CANCELLED);
|
||||
return true;
|
||||
}
|
||||
|
||||
synchronized protected boolean isCancelled() {
|
||||
protected void updateState(Worker.State newState) {
|
||||
state.set(newState);
|
||||
}
|
||||
|
||||
protected synchronized boolean isCancelled() {
|
||||
return getState() == Worker.State.CANCELLED;
|
||||
}
|
||||
}
|
||||
@ -626,7 +632,7 @@ public final class ImageGalleryController implements Executor {
|
||||
/**
|
||||
* Abstract base class for tasks associated with a file in the database
|
||||
*/
|
||||
static public abstract class FileTask extends InnerTask {
|
||||
static public abstract class FileTask extends BackgroundTask {
|
||||
|
||||
private final AbstractFile file;
|
||||
private final DrawableDB taskDB;
|
||||
@ -644,7 +650,6 @@ public final class ImageGalleryController implements Executor {
|
||||
this.file = f;
|
||||
this.taskDB = taskDB;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -701,63 +706,63 @@ public final class ImageGalleryController implements Executor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task that runs when image gallery listening is (re) enabled.
|
||||
*
|
||||
* Grabs all files with supported image/video mime types or extensions, and
|
||||
* adds them to the Drawable DB. Uses the presence of a mimetype as an
|
||||
* approximation to 'analyzed'.
|
||||
*/
|
||||
@NbBundle.Messages({"CopyAnalyzedFiles.populatingDb.status=populating analyzed image/video database",
|
||||
"CopyAnalyzedFiles.committingDb.status=commiting 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 InnerTask {
|
||||
@NbBundle.Messages({"BulkTask.committingDb.status=commiting image/video database",
|
||||
"BulkTask.stopCopy.status=Stopping copy to drawable db task.",
|
||||
"BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."})
|
||||
abstract static private class BulkTransferTask extends BackgroundTask {
|
||||
|
||||
private final ImageGalleryController controller;
|
||||
private final DrawableDB taskDB;
|
||||
private final SleuthkitCase tskCase;
|
||||
static private final String FILE_EXTENSION_CLAUSE =
|
||||
"(name LIKE '%." //NON-NLS
|
||||
+ String.join("' OR name LIKE '%.", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS
|
||||
+ "')";
|
||||
|
||||
CopyAnalyzedFiles(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
|
||||
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
|
||||
|
||||
final ImageGalleryController controller;
|
||||
final DrawableDB taskDB;
|
||||
final SleuthkitCase tskCase;
|
||||
|
||||
ProgressHandle progressHandle;
|
||||
|
||||
BulkTransferTask(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
|
||||
this.controller = controller;
|
||||
this.taskDB = taskDB;
|
||||
this.tskCase = tskCase;
|
||||
}
|
||||
|
||||
static private final String FILE_EXTENSION_CLAUSE =
|
||||
"(name LIKE '%." //NON-NLS
|
||||
+ StringUtils.join(FileTypeUtils.getAllSupportedExtensions(), "' OR name LIKE '%.") //NON-NLS
|
||||
+ "')";
|
||||
abstract void cleanup(boolean success);
|
||||
|
||||
static private final String MIMETYPE_CLAUSE =
|
||||
"(mime_type LIKE '" //NON-NLS
|
||||
+ StringUtils.join(FileTypeUtils.getAllSupportedMimeTypes(), "' OR mime_type LIKE '") //NON-NLS
|
||||
+ "') ";
|
||||
abstract List<AbstractFile> getFiles() throws TskCoreException;
|
||||
|
||||
static private 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 ProgressHandle progressHandle = ProgressHandleFactory.createHandle(Bundle.CopyAnalyzedFiles_populatingDb_status());
|
||||
abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) throws TskCoreException;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
progressHandle = getInitialProgressHandle();
|
||||
progressHandle.start();
|
||||
updateMessage(Bundle.CopyAnalyzedFiles_populatingDb_status());
|
||||
|
||||
try {
|
||||
//grab all files with supported extension or detected mime types
|
||||
final List<AbstractFile> files = tskCase.findAllFilesWhere(DRAWABLE_QUERY);
|
||||
final List<AbstractFile> files = getFiles();
|
||||
progressHandle.switchToDeterminate(files.size());
|
||||
|
||||
updateProgress(0.0);
|
||||
|
||||
//do in transaction
|
||||
DrawableDB.DrawableTransaction tr = taskDB.beginTransaction();
|
||||
int units = 0;
|
||||
int workDone = 0;
|
||||
for (final AbstractFile f : files) {
|
||||
if (isCancelled()) {
|
||||
LOGGER.log(Level.WARNING, "Task cancelled: not all contents may be transfered to drawable database."); //NON-NLS
|
||||
@ -765,161 +770,172 @@ public final class ImageGalleryController implements Executor {
|
||||
break;
|
||||
}
|
||||
|
||||
final boolean known = f.getKnown() == TskData.FileKnown.KNOWN;
|
||||
processFile(f, tr);
|
||||
|
||||
if (known) {
|
||||
taskDB.removeFile(f.getId(), tr); //remove known files
|
||||
} else {
|
||||
Optional<String> mimeType = FileTypeUtils.getMimeType(f);
|
||||
if (mimeType.isPresent()) {
|
||||
//mime type
|
||||
if (FileTypeUtils.isDrawableMimeType(mimeType.get())) { //supported mimetype => analyzed
|
||||
taskDB.updateFile(DrawableFile.create(f, true, false), tr);
|
||||
} else { //unsupported mimtype => analyzed but shouldn't include
|
||||
taskDB.removeFile(f.getId(), tr);
|
||||
}
|
||||
} else {
|
||||
//no mime tyoe
|
||||
if (FileTypeUtils.isDrawable(f)) {
|
||||
//no mime type but supported => add as not analyzed
|
||||
taskDB.insertFile(DrawableFile.create(f, false, false), tr);
|
||||
} else {
|
||||
//no mime type, not supported => remove ( should never get here)
|
||||
taskDB.removeFile(f.getId(), tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
units++;
|
||||
final int prog = units;
|
||||
progressHandle.progress(f.getName(), units);
|
||||
updateProgress(prog - 1 / (double) files.size());
|
||||
workDone++;
|
||||
progressHandle.progress(f.getName(), workDone);
|
||||
updateProgress(workDone - 1 / (double) files.size());
|
||||
updateMessage(f.getName());
|
||||
}
|
||||
|
||||
progressHandle.finish();
|
||||
|
||||
progressHandle = ProgressHandleFactory.createHandle(Bundle.CopyAnalyzedFiles_committingDb_status());
|
||||
updateMessage(Bundle.CopyAnalyzedFiles_committingDb_status());
|
||||
progressHandle = ProgressHandleFactory.createHandle(Bundle.BulkTask_committingDb_status());
|
||||
updateMessage(Bundle.BulkTask_committingDb_status());
|
||||
updateProgress(1.0);
|
||||
|
||||
progressHandle.start();
|
||||
taskDB.commitTransaction(tr, true);
|
||||
|
||||
} catch (TskCoreException ex) {
|
||||
progressHandle.progress(Bundle.CopyAnalyzedFiles_stopCopy_status());
|
||||
Logger.getLogger(CopyAnalyzedFiles.class.getName()).log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents: " + ex.getMessage()); //NON-NLS
|
||||
MessageNotifyUtil.Notify.warn(Bundle.CopyAnalyzedFiles_errPopulating_errMsg(), ex.getMessage());
|
||||
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
|
||||
MessageNotifyUtil.Notify.warn(Bundle.BulkTask_errPopulating_errMsg(), ex.getMessage());
|
||||
cleanup(false);
|
||||
return;
|
||||
} finally {
|
||||
progressHandle.finish();
|
||||
updateMessage("");
|
||||
updateProgress(-1.0);
|
||||
controller.setStale(true);
|
||||
return;
|
||||
}
|
||||
cleanup(true);
|
||||
}
|
||||
|
||||
progressHandle.finish();
|
||||
updateMessage("");
|
||||
updateProgress(-1.0);
|
||||
controller.setStale(false);
|
||||
abstract ProgressHandle getInitialProgressHandle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Task that runs when image gallery listening is (re) enabled.
|
||||
*
|
||||
* Grabs all files with supported image/video mime types or extensions, and
|
||||
* adds them to the Drawable DB. Uses the presence of a mimetype as an
|
||||
* approximation to 'analyzed'.
|
||||
*/
|
||||
@NbBundle.Messages({"CopyAnalyzedFiles.committingDb.status=commiting 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 {
|
||||
|
||||
CopyAnalyzedFiles(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
|
||||
super(controller, taskDB, tskCase);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(boolean success) {
|
||||
controller.setStale(!success);
|
||||
}
|
||||
|
||||
@Override
|
||||
List<AbstractFile> getFiles() throws TskCoreException {
|
||||
return tskCase.findAllFilesWhere(DRAWABLE_QUERY);
|
||||
}
|
||||
|
||||
@Override
|
||||
void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr) throws TskCoreException {
|
||||
final boolean known = f.getKnown() == TskData.FileKnown.KNOWN;
|
||||
|
||||
if (known) {
|
||||
taskDB.removeFile(f.getId(), tr); //remove known files
|
||||
} else {
|
||||
Optional<String> mimeType = FileTypeUtils.getMimeType(f);
|
||||
if (mimeType.isPresent()) {
|
||||
//mime type
|
||||
if (FileTypeUtils.isDrawableMimeType(mimeType.get())) { //supported mimetype => analyzed
|
||||
taskDB.updateFile(DrawableFile.create(f, true, false), tr);
|
||||
} else { //unsupported mimtype => analyzed but shouldn't include
|
||||
taskDB.removeFile(f.getId(), tr);
|
||||
}
|
||||
} else {
|
||||
//no mime tyoe
|
||||
if (FileTypeUtils.isDrawable(f)) {
|
||||
//no mime type but supported => add as not analyzed
|
||||
taskDB.insertFile(DrawableFile.create(f, false, false), tr);
|
||||
} else {
|
||||
//no mime type, not supported => remove ( should never get here)
|
||||
taskDB.removeFile(f.getId(), tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NbBundle.Messages({"CopyAnalyzedFiles.populatingDb.status=populating analyzed image/video database",})
|
||||
ProgressHandle getInitialProgressHandle() {
|
||||
return ProgressHandleFactory.createHandle(Bundle.CopyAnalyzedFiles_populatingDb_status(), this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* task that does pre-ingest copy over of files from a new datasource (uses
|
||||
* fs_obj_id to identify files from new datasources)
|
||||
* 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.prepopulatingDb.status=prepopulating image/video database",
|
||||
"PrePopulateDataSourceFiles.committingDb.status=commiting image/video database"})
|
||||
private class PrePopulateDataSourceFiles extends InnerTask {
|
||||
@NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=commiting image/video database"})
|
||||
static private class PrePopulateDataSourceFiles extends BulkTransferTask {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(PrePopulateDataSourceFiles.class.getName());
|
||||
|
||||
private final Content dataSource;
|
||||
|
||||
/**
|
||||
* here we grab by extension but in file_done listener we look at file
|
||||
* type id attributes but fall back on jpeg signatures and extensions to
|
||||
* check for supported images
|
||||
*/
|
||||
// (name like '.jpg' or name like '.png' ...)
|
||||
private final String DRAWABLE_QUERY = "(name LIKE '%." + StringUtils.join(FileTypeUtils.getAllSupportedExtensions(), "' OR name LIKE '%.") + "') "; //NON-NLS
|
||||
|
||||
private ProgressHandle progressHandle = ProgressHandleFactory.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param dataSourceId Data source object ID
|
||||
*/
|
||||
PrePopulateDataSourceFiles(Content dataSource) {
|
||||
super();
|
||||
PrePopulateDataSourceFiles(Content dataSource, ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
|
||||
super(controller, taskDB, tskCase);
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy files from a newly added data source into the DB. Get all
|
||||
* "drawable" files, based on extension. After ingest we use file type
|
||||
* id module and if necessary jpeg/png signature matching to add/remove
|
||||
* files
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
progressHandle.start();
|
||||
updateMessage(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status());
|
||||
protected void cleanup(boolean success) {
|
||||
}
|
||||
|
||||
try {
|
||||
String fsQuery = "(fs_obj_id IS NULL) "; //default clause NON-NLS
|
||||
@Override
|
||||
void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) {
|
||||
taskDB.insertFile(DrawableFile.create(f, false, false), tr);
|
||||
}
|
||||
|
||||
@Override
|
||||
List<AbstractFile> getFiles() throws TskCoreException {
|
||||
if (dataSource instanceof Image) {
|
||||
List<FileSystem> fileSystems = ((Image) dataSource).getFileSystems();
|
||||
if (fileSystems.isEmpty()) {
|
||||
/*
|
||||
* no filesystems, don't bother with the initial population,
|
||||
* just sort things out on file_done events
|
||||
*/
|
||||
progressHandle.finish();
|
||||
return Collections.emptyList();
|
||||
}
|
||||
//use this clause to only grab files from the newly added filesystems.
|
||||
String fsQuery = fileSystems.stream()
|
||||
.map(fileSystem -> String.valueOf(fileSystem.getId()))
|
||||
.collect(Collectors.joining(" OR fs_obj_id = ", "(fs_obj_id = ", ") ")); //NON-NLS
|
||||
|
||||
return tskCase.findAllFilesWhere(fsQuery + " AND " + DRAWABLE_QUERY); //NON-NLS
|
||||
} else if (dataSource instanceof VirtualDirectory) {
|
||||
/*
|
||||
* NOTE: Logical files currently (Apr '15) have a null value for
|
||||
* fs_obj_id in DB. for them, we will not specify a fs_obj_id,
|
||||
* which means we will grab files from another data source, but
|
||||
* the drawable DB is smart enough to de-dupe them. For Images
|
||||
* we can do better.
|
||||
* fs_obj_id is set only for file system files, so we will match
|
||||
* the VirtualDirectory's name in the parent path.
|
||||
*
|
||||
* TODO: A future database schema could probably make this
|
||||
* cleaner. If we had a datasource_id column in the files table
|
||||
* we could just match agains that.
|
||||
*/
|
||||
if (dataSource instanceof Image) {
|
||||
List<FileSystem> fileSystems = ((Image) dataSource).getFileSystems();
|
||||
if (fileSystems.isEmpty()) {
|
||||
/*
|
||||
* no filesystems, don't bother with the initial
|
||||
* population, just sort things out on file_done events
|
||||
*/
|
||||
progressHandle.finish();
|
||||
return;
|
||||
}
|
||||
//use this clause to only grab files from the newly added filesystems.
|
||||
fsQuery = fileSystems.stream()
|
||||
.map(fileSystem -> String.valueOf(fileSystem.getId()))
|
||||
.collect(Collectors.joining(" OR fs_obj_id = ", "(fs_obj_id = ", ") ")); //NON-NLS
|
||||
}
|
||||
|
||||
final List<AbstractFile> files = getSleuthKitCase().findAllFilesWhere(fsQuery + " AND " + DRAWABLE_QUERY); //NON-NLS
|
||||
progressHandle.switchToDeterminate(files.size());
|
||||
|
||||
//do in transaction
|
||||
DrawableDB.DrawableTransaction tr = db.beginTransaction();
|
||||
int units = 0;
|
||||
for (final AbstractFile f : files) {
|
||||
if (isCancelled()) {
|
||||
LOGGER.log(Level.WARNING, "task cancelled: not all contents may be transfered to database"); //NON-NLS
|
||||
progressHandle.finish();
|
||||
break;
|
||||
}
|
||||
db.insertFile(DrawableFile.create(f, false, false), tr);
|
||||
units++;
|
||||
progressHandle.progress(f.getName(), units);
|
||||
}
|
||||
|
||||
progressHandle.finish();
|
||||
progressHandle = ProgressHandleFactory.createHandle(Bundle.PrePopulateDataSourceFiles_committingDb_status());
|
||||
|
||||
progressHandle.start();
|
||||
db.commitTransaction(tr, false);
|
||||
|
||||
} catch (TskCoreException ex) {
|
||||
Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.WARNING, "failed to transfer all database contents", ex); //NON-NLS
|
||||
return tskCase.findAllFilesWhere(" parent_path LIKE '/" + dataSource.getName() + "/%' AND " + DRAWABLE_QUERY); //NON-NLS
|
||||
} else {
|
||||
String msg = "Uknown datasource type: " + dataSource.getClass().getName();
|
||||
LOGGER.log(Level.SEVERE, msg);
|
||||
throw new IllegalArgumentException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
progressHandle.finish();
|
||||
@Override
|
||||
@NbBundle.Messages({"PrePopulateDataSourceFiles.prepopulatingDb.status=prepopulating image/video database",})
|
||||
ProgressHandle getInitialProgressHandle() {
|
||||
return ProgressHandleFactory.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1011,7 +1027,7 @@ public final class ImageGalleryController implements Executor {
|
||||
//copy all file data to drawable databse
|
||||
Content newDataSource = (Content) evt.getNewValue();
|
||||
if (isListeningEnabled()) {
|
||||
queueDBWorkerTask(new PrePopulateDataSourceFiles(newDataSource));
|
||||
queueDBWorkerTask(new PrePopulateDataSourceFiles(newDataSource, ImageGalleryController.this, getDatabase(), getSleuthKitCase()));
|
||||
} else {//TODO: keep track of what we missed for later
|
||||
setStale(true);
|
||||
}
|
||||
@ -1028,7 +1044,6 @@ public final class ImageGalleryController implements Executor {
|
||||
getTagsManager().fireTagDeletedEvent(tagDeletedEvent);
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,14 +157,15 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
|
||||
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.0, 1.0);
|
||||
splitPane.setDividerPositions(0.1, 1.0);
|
||||
|
||||
ImageGalleryController.getDefault().setStacks(fullUIStack, centralStack);
|
||||
ImageGalleryController.getDefault().setShowTree(() -> tabPane.getSelectionModel().select(groupTree));
|
||||
|
@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2013-15 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.actions;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Menu;
|
||||
import javax.swing.SwingWorker;
|
||||
|
||||
import org.openide.util.NbBundle;
|
||||
import org.openide.util.Utilities;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
|
||||
import org.sleuthkit.datamodel.AbstractFile;
|
||||
import org.sleuthkit.datamodel.ContentTag;
|
||||
import org.sleuthkit.datamodel.TagName;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
|
||||
/**
|
||||
* Instances of this Action allow users to apply tags to content.
|
||||
*
|
||||
* //TODO: since we are not using actionsGlobalContext anymore and this has
|
||||
* diverged from autopsy action, make this extend from controlsfx Action
|
||||
*/
|
||||
public class AddDrawableTagAction extends AddTagAction {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(AddDrawableTagAction.class.getName());
|
||||
|
||||
private final ImageGalleryController controller;
|
||||
|
||||
public AddDrawableTagAction(ImageGalleryController controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
public Menu getPopupMenu() {
|
||||
return new TagMenu(controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NbBundle.Messages({"AddDrawableTagAction.displayName.plural=Tag Files",
|
||||
"AddDrawableTagAction.displayName.singular=Tag File"})
|
||||
protected String getActionDisplayName() {
|
||||
return Utilities.actionsGlobalContext().lookupAll(AbstractFile.class).size() > 1
|
||||
? Bundle.AddDrawableTagAction_displayName_plural()
|
||||
: Bundle.AddDrawableTagAction_displayName_singular();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTag(TagName tagName, String comment) {
|
||||
Set<Long> selectedFiles = new HashSet<>(controller.getSelectionModel().getSelected());
|
||||
addTagsToFiles(tagName, comment, selectedFiles);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NbBundle.Messages({"# {0} - fileID",
|
||||
"AddDrawableTagAction.addTagsToFiles.alert=Unable to tag file {0}."})
|
||||
public void addTagsToFiles(TagName tagName, String comment, Set<Long> selectedFiles) {
|
||||
new SwingWorker<Void, Void>() {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground() throws Exception {
|
||||
for (Long fileID : selectedFiles) {
|
||||
try {
|
||||
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
|
||||
|
||||
// check if the same tag is being added for the same abstract file.
|
||||
DrawableTagsManager tagsManager = controller.getTagsManager();
|
||||
List<ContentTag> contentTags = tagsManager.getContentTagsByContent(file);
|
||||
Optional<TagName> duplicateTagName = contentTags.stream()
|
||||
.map(ContentTag::getName)
|
||||
.filter(tagName::equals)
|
||||
.findAny();
|
||||
|
||||
if (duplicateTagName.isPresent()) {
|
||||
LOGGER.log(Level.INFO, "{0} already tagged as {1}. Skipping.", new Object[]{file.getName(), tagName.getDisplayName()}); //NON-NLS
|
||||
} else {
|
||||
LOGGER.log(Level.INFO, "Tagging {0} as {1}", new Object[]{file.getName(), tagName.getDisplayName()}); //NON-NLS
|
||||
controller.getTagsManager().addContentTag(file, tagName, comment);
|
||||
}
|
||||
|
||||
} catch (TskCoreException tskCoreException) {
|
||||
LOGGER.log(Level.SEVERE, "Error tagging file", tskCoreException); //NON-NLS
|
||||
Platform.runLater(() -> {
|
||||
new Alert(Alert.AlertType.ERROR, Bundle.AddDrawableTagAction_addTagsToFiles_alert(fileID)).show();
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
super.done();
|
||||
try {
|
||||
get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
LOGGER.log(Level.SEVERE, "unexpected exception while tagging files", ex); //NON-NLS
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2013 Basis Technology Corp.
|
||||
* Copyright 2013-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -20,67 +20,128 @@ package org.sleuthkit.autopsy.imagegallery.actions;
|
||||
|
||||
import java.awt.Window;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javafx.event.ActionEvent;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
import javax.swing.SwingWorker;
|
||||
import org.controlsfx.control.action.Action;
|
||||
import org.controlsfx.control.action.ActionUtils;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.openide.windows.TopComponent;
|
||||
import org.openide.windows.WindowManager;
|
||||
import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog;
|
||||
import org.sleuthkit.autopsy.actions.GetTagNameDialog;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
|
||||
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.TskCoreException;
|
||||
|
||||
/**
|
||||
* An abstract base class for actions that allow users to tag SleuthKit data
|
||||
* model objects.
|
||||
*
|
||||
* //TODO: this class started as a cut and paste from
|
||||
* org.sleuthkit.autopsy.actions.AddTagAction and needs to be refactored or
|
||||
* reintegrated to the AddTagAction hierarchy of Autopysy.
|
||||
* Instances of this Action allow users to apply tags to content.
|
||||
*/
|
||||
abstract class AddTagAction {
|
||||
public class AddTagAction extends Action {
|
||||
|
||||
protected static final String NO_COMMENT = "";
|
||||
private static final Logger LOGGER = Logger.getLogger(AddTagAction.class.getName());
|
||||
|
||||
/**
|
||||
* Template method to allow derived classes to provide a string for a menu
|
||||
* item label.
|
||||
*/
|
||||
abstract protected String getActionDisplayName();
|
||||
private final ImageGalleryController controller;
|
||||
private final Set<Long> selectedFileIDs;
|
||||
private final TagName tagName;
|
||||
|
||||
/**
|
||||
* Template method to allow derived classes to add the indicated tag and
|
||||
* comment to one or more a SleuthKit data model objects.
|
||||
*/
|
||||
abstract protected void addTag(TagName tagName, String comment);
|
||||
public AddTagAction(ImageGalleryController controller, TagName tagName, Set<Long> selectedFileIDs) {
|
||||
super(tagName.getDisplayName());
|
||||
this.controller = controller;
|
||||
this.selectedFileIDs = selectedFileIDs;
|
||||
this.tagName = tagName;
|
||||
setGraphic(controller.getTagsManager().getGraphic(tagName));
|
||||
setText(tagName.getDisplayName());
|
||||
setEventHandler(actionEvent -> addTagWithComment(""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Template method to allow derived classes to add the indicated tag and
|
||||
* comment to a list of one or more file IDs.
|
||||
*/
|
||||
abstract protected void addTagsToFiles(TagName tagName, String comment, Set<Long> selectedFiles);
|
||||
static public Menu getTagMenu(ImageGalleryController controller) {
|
||||
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) {
|
||||
new SwingWorker<Void, Void>() {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground() throws Exception {
|
||||
// check if the same tag is being added for the same abstract file.
|
||||
DrawableTagsManager tagsManager = controller.getTagsManager();
|
||||
for (Long fileID : selectedFiles) {
|
||||
try {
|
||||
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.getContentTagsByContent(file);
|
||||
Optional<TagName> duplicateTagName = contentTags.stream()
|
||||
.map(ContentTag::getName)
|
||||
.filter(tagName::equals)
|
||||
.findAny();
|
||||
|
||||
if (duplicateTagName.isPresent()) {
|
||||
LOGGER.log(Level.INFO, "{0} already tagged as {1}. Skipping.", new Object[]{file.getName(), tagName.getDisplayName()}); //NON-NLS
|
||||
} else {
|
||||
LOGGER.log(Level.INFO, "Tagging {0} as {1}", new Object[]{file.getName(), tagName.getDisplayName()}); //NON-NLS
|
||||
controller.getTagsManager().addContentTag(file, tagName, comment);
|
||||
}
|
||||
|
||||
} catch (TskCoreException tskCoreException) {
|
||||
LOGGER.log(Level.SEVERE, "Error tagging file", tskCoreException); //NON-NLS
|
||||
Platform.runLater(() ->
|
||||
new Alert(Alert.AlertType.ERROR, Bundle.AddDrawableTagAction_addTagsToFiles_alert(fileID)).show()
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
super.done();
|
||||
try {
|
||||
get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
LOGGER.log(Level.SEVERE, "unexpected exception while tagging files", ex); //NON-NLS
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances of this class implement a context menu user interface for
|
||||
* creating or selecting a tag name for a tag and specifying an optional tag
|
||||
* comment.
|
||||
*/
|
||||
// @@@ This user interface has some significant usability issues and needs
|
||||
// to be reworked.
|
||||
@NbBundle.Messages({"AddTagAction.menuItem.quickTag=Quick Tag",
|
||||
"AddTagAction.menuItem.noTags=No tags",
|
||||
"AddTagAction.menuItem.newTag=New Tag...",
|
||||
"AddTagAction.menuItem.tagAndComment=Tag and Comment..."})
|
||||
protected class TagMenu extends Menu {
|
||||
"AddTagAction.menuItem.noTags=No tags",
|
||||
"AddTagAction.menuItem.newTag=New Tag...",
|
||||
"AddTagAction.menuItem.tagAndComment=Tag and Comment...",
|
||||
"AddDrawableTagAction.displayName.plural=Tag Files",
|
||||
"AddDrawableTagAction.displayName.singular=Tag File"})
|
||||
private static class TagMenu extends Menu {
|
||||
|
||||
TagMenu(ImageGalleryController controller) {
|
||||
super(getActionDisplayName());
|
||||
setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon()));
|
||||
ObservableSet<Long> selectedFileIDs = controller.getSelectionModel().getSelected();
|
||||
setText(selectedFileIDs.size() > 1
|
||||
? Bundle.AddDrawableTagAction_displayName_plural()
|
||||
: Bundle.AddDrawableTagAction_displayName_singular());
|
||||
|
||||
// Create a "Quick Tag" sub-menu.
|
||||
Menu quickTagMenu = new Menu(Bundle.AddTagAction_menuItem_quickTag());
|
||||
@ -98,10 +159,8 @@ abstract class AddTagAction {
|
||||
quickTagMenu.getItems().add(empty);
|
||||
} else {
|
||||
for (final TagName tagName : tagNames) {
|
||||
MenuItem tagNameItem = new MenuItem(tagName.getDisplayName());
|
||||
tagNameItem.setOnAction((ActionEvent t) -> {
|
||||
addTag(tagName, NO_COMMENT);
|
||||
});
|
||||
AddTagAction addDrawableTagAction = new AddTagAction(controller, tagName, selectedFileIDs);
|
||||
MenuItem tagNameItem = ActionUtils.createMenuItem(addDrawableTagAction);
|
||||
quickTagMenu.getItems().add(tagNameItem);
|
||||
}
|
||||
}
|
||||
@ -112,14 +171,13 @@ abstract class AddTagAction {
|
||||
* or select a tag name and adds a tag with the resulting name.
|
||||
*/
|
||||
MenuItem newTagMenuItem = new MenuItem(Bundle.AddTagAction_menuItem_newTag());
|
||||
newTagMenuItem.setOnAction((ActionEvent t) -> {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
TagName tagName = GetTagNameDialog.doDialog(getIGWindow());
|
||||
if (tagName != null) {
|
||||
addTag(tagName, NO_COMMENT);
|
||||
}
|
||||
});
|
||||
});
|
||||
newTagMenuItem.setOnAction(actionEvent ->
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
TagName tagName = GetTagNameDialog.doDialog(getIGWindow());
|
||||
if (tagName != null) {
|
||||
new AddTagAction(controller, tagName, selectedFileIDs).handle(actionEvent);
|
||||
}
|
||||
}));
|
||||
quickTagMenu.getItems().add(newTagMenuItem);
|
||||
|
||||
/*
|
||||
@ -129,26 +187,17 @@ abstract class AddTagAction {
|
||||
* name.
|
||||
*/
|
||||
MenuItem tagAndCommentItem = new MenuItem(Bundle.AddTagAction_menuItem_tagAndComment());
|
||||
tagAndCommentItem.setOnAction((ActionEvent t) -> {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
GetTagNameAndCommentDialog.TagNameAndComment tagNameAndComment = GetTagNameAndCommentDialog.doDialog(getIGWindow());
|
||||
if (null != tagNameAndComment) {
|
||||
if (CategoryManager.isCategoryTagName(tagNameAndComment.getTagName())) {
|
||||
new CategorizeAction(controller).addTag(tagNameAndComment.getTagName(), tagNameAndComment.getComment());
|
||||
} else {
|
||||
new AddDrawableTagAction(controller).addTag(tagNameAndComment.getTagName(), tagNameAndComment.getComment());
|
||||
tagAndCommentItem.setOnAction(actionEvent ->
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
GetTagNameAndCommentDialog.TagNameAndComment tagNameAndComment = GetTagNameAndCommentDialog.doDialog(getIGWindow());
|
||||
if (null != tagNameAndComment) {
|
||||
new AddTagAction(controller, tagNameAndComment.getTagName(), selectedFileIDs).addTagWithComment(tagNameAndComment.getComment());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
getItems().add(tagAndCommentItem);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Window containing the ImageGalleryTopComponent
|
||||
*/
|
||||
static private Window getIGWindow() {
|
||||
TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID);
|
||||
return SwingUtilities.getWindowAncestor(etc);
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2013 Basis Technology Corp.
|
||||
* Copyright 2013-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -21,26 +21,28 @@ package org.sleuthkit.autopsy.imagegallery.actions;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.swing.JOptionPane;
|
||||
|
||||
import org.controlsfx.control.action.Action;
|
||||
import org.controlsfx.control.action.ActionUtils;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
|
||||
import org.sleuthkit.datamodel.ContentTag;
|
||||
@ -49,48 +51,42 @@ import org.sleuthkit.datamodel.TagName;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
|
||||
/**
|
||||
* Adaptation of Tag Actions to enforce category-tag uniqueness
|
||||
*
|
||||
* TODO: since we are not using actionsGlobalContext anymore and this has
|
||||
* diverged from autopsy action, make this extend from controlsfx Action
|
||||
*/
|
||||
@NbBundle.Messages({"CategorizeAction.displayName=Categorize"})
|
||||
public class CategorizeAction extends AddTagAction {
|
||||
public class CategorizeAction extends Action {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(CategorizeAction.class.getName());
|
||||
|
||||
private final ImageGalleryController controller;
|
||||
private final UndoRedoManager undoManager;
|
||||
private final Category cat;
|
||||
private final Set<Long> selectedFileIDs;
|
||||
private final Boolean createUndo;
|
||||
|
||||
public CategorizeAction(ImageGalleryController controller) {
|
||||
super();
|
||||
this.controller = controller;
|
||||
undoManager = controller.getUndoManager();
|
||||
public CategorizeAction(ImageGalleryController controller, Category cat, Set<Long> selectedFileIDs) {
|
||||
this(controller, cat, selectedFileIDs, true);
|
||||
}
|
||||
|
||||
public Menu getPopupMenu() {
|
||||
private CategorizeAction(ImageGalleryController controller, Category cat, Set<Long> selectedFileIDs, Boolean createUndo) {
|
||||
super(cat.getDisplayName());
|
||||
this.controller = controller;
|
||||
this.undoManager = controller.getUndoManager();
|
||||
this.cat = cat;
|
||||
this.selectedFileIDs = selectedFileIDs;
|
||||
this.createUndo = createUndo;
|
||||
setGraphic(cat.getGraphic());
|
||||
setEventHandler(actionEvent -> addCatToFiles());
|
||||
setAccelerator(new KeyCodeCombination(KeyCode.getKeyCode(Integer.toString(cat.getCategoryNumber()))));
|
||||
}
|
||||
|
||||
static public Menu getCategoriesMenu(ImageGalleryController controller) {
|
||||
return new CategoryMenu(controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getActionDisplayName() {
|
||||
return Bundle.CategorizeAction_displayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTag(TagName tagName, String comment) {
|
||||
Set<Long> selectedFiles = new HashSet<>(controller.getSelectionModel().getSelected());
|
||||
addTagsToFiles(tagName, comment, selectedFiles);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addTagsToFiles(TagName tagName, String comment, Set<Long> selectedFiles) {
|
||||
addTagsToFiles(tagName, comment, selectedFiles, true);
|
||||
}
|
||||
|
||||
public void addTagsToFiles(TagName tagName, String comment, Set<Long> selectedFiles, boolean createUndo) {
|
||||
Logger.getAnonymousLogger().log(Level.INFO, "categorizing{0} as {1}", new Object[]{selectedFiles.toString(), tagName.getDisplayName()}); //NON-NLS
|
||||
controller.queueDBWorkerTask(new CategorizeTask(selectedFiles, tagName, comment, createUndo));
|
||||
private void addCatToFiles() {
|
||||
Logger.getAnonymousLogger().log(Level.INFO, "categorizing{0} as {1}", new Object[]{selectedFileIDs.toString(), cat.getDisplayName()}); //NON-NLS
|
||||
controller.queueDBWorkerTask(new CategorizeTask(selectedFileIDs, cat, createUndo));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -101,49 +97,43 @@ public class CategorizeAction extends AddTagAction {
|
||||
|
||||
CategoryMenu(ImageGalleryController controller) {
|
||||
super(Bundle.CategorizeAction_displayName());
|
||||
setGraphic(new ImageView(DrawableAttribute.CATEGORY.getIcon()));
|
||||
ObservableSet<Long> selected = controller.getSelectionModel().getSelected();
|
||||
|
||||
// Each category get an item in the sub-menu. Selecting one of these menu items adds
|
||||
// a tag with the associated category.
|
||||
for (final Category cat : Category.values()) {
|
||||
|
||||
MenuItem categoryItem = new MenuItem(cat.getDisplayName());
|
||||
categoryItem.setOnAction((ActionEvent t) -> {
|
||||
final CategorizeAction categorizeAction = new CategorizeAction(controller);
|
||||
categorizeAction.addTag(controller.getCategoryManager().getTagName(cat), NO_COMMENT);
|
||||
});
|
||||
categoryItem.setAccelerator(new KeyCodeCombination(KeyCode.getKeyCode(Integer.toString(cat.getCategoryNumber()))));
|
||||
MenuItem categoryItem = ActionUtils.createMenuItem(new CategorizeAction(controller, cat, selected));
|
||||
getItems().add(categoryItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NbBundle.Messages({"# {0} - fileID number",
|
||||
"CategorizeTask.errorUnable.msg=Unable to categorize {0}.",
|
||||
"CategorizeTask.errorUnable.title=Categorizing Error"})
|
||||
private class CategorizeTask extends ImageGalleryController.InnerTask {
|
||||
"CategorizeTask.errorUnable.msg=Unable to categorize {0}.",
|
||||
"CategorizeTask.errorUnable.title=Categorizing Error"})
|
||||
private class CategorizeTask extends ImageGalleryController.BackgroundTask {
|
||||
|
||||
private final Set<Long> fileIDs;
|
||||
@Nonnull
|
||||
private final TagName tagName;
|
||||
private final String comment;
|
||||
private final boolean createUndo;
|
||||
|
||||
CategorizeTask(Set<Long> fileIDs, @Nonnull TagName tagName, String comment, boolean createUndo) {
|
||||
private final boolean createUndo;
|
||||
private final Category cat;
|
||||
|
||||
CategorizeTask(Set<Long> fileIDs, @Nonnull Category cat, boolean createUndo) {
|
||||
super();
|
||||
this.fileIDs = fileIDs;
|
||||
java.util.Objects.requireNonNull(tagName);
|
||||
this.tagName = tagName;
|
||||
this.comment = comment;
|
||||
java.util.Objects.requireNonNull(cat);
|
||||
this.cat = cat;
|
||||
this.createUndo = createUndo;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final DrawableTagsManager tagsManager = controller.getTagsManager();
|
||||
final CategoryManager categoryManager = controller.getCategoryManager();
|
||||
Map<Long, TagName> oldCats = new HashMap<>();
|
||||
Map<Long, Category> oldCats = new HashMap<>();
|
||||
TagName tagName = categoryManager.getTagName(cat);
|
||||
TagName catZeroTagName = categoryManager.getTagName(Category.ZERO);
|
||||
for (long fileID : fileIDs) {
|
||||
try {
|
||||
DrawableFile<?> file = controller.getFileFromId(fileID); //drawable db access
|
||||
@ -151,12 +141,12 @@ public class CategorizeAction extends AddTagAction {
|
||||
Category oldCat = file.getCategory(); //drawable db access
|
||||
TagName oldCatTagName = categoryManager.getTagName(oldCat);
|
||||
if (false == tagName.equals(oldCatTagName)) {
|
||||
oldCats.put(fileID, oldCatTagName);
|
||||
oldCats.put(fileID, oldCat);
|
||||
}
|
||||
}
|
||||
|
||||
final List<ContentTag> fileTags = tagsManager.getContentTagsByContent(file);
|
||||
if (tagName == categoryManager.getTagName(Category.ZERO)) {
|
||||
if (tagName.equals(catZeroTagName)) {
|
||||
// delete all cat tags for cat-0
|
||||
fileTags.stream()
|
||||
.filter(tag -> CategoryManager.isCategoryTagName(tag.getName()))
|
||||
@ -173,7 +163,7 @@ public class CategorizeAction extends AddTagAction {
|
||||
.map(Tag::getName)
|
||||
.filter(tagName::equals)
|
||||
.collect(Collectors.toList()).isEmpty()) {
|
||||
tagsManager.addContentTag(file, tagName, comment);
|
||||
tagsManager.addContentTag(file, tagName, "");
|
||||
}
|
||||
}
|
||||
} catch (TskCoreException ex) {
|
||||
@ -186,7 +176,7 @@ public class CategorizeAction extends AddTagAction {
|
||||
}
|
||||
|
||||
if (createUndo && oldCats.isEmpty() == false) {
|
||||
undoManager.addToUndo(new CategorizationChange(controller, tagName, oldCats));
|
||||
undoManager.addToUndo(new CategorizationChange(controller, cat, oldCats));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,11 +187,11 @@ public class CategorizeAction extends AddTagAction {
|
||||
@Immutable
|
||||
private final class CategorizationChange implements UndoRedoManager.UndoableCommand {
|
||||
|
||||
private final TagName newCategory;
|
||||
private final ImmutableMap<Long, TagName> oldCategories;
|
||||
private final Category newCategory;
|
||||
private final ImmutableMap<Long, Category> oldCategories;
|
||||
private final ImageGalleryController controller;
|
||||
|
||||
CategorizationChange(ImageGalleryController controller, TagName newCategory, Map<Long, TagName> oldCategories) {
|
||||
CategorizationChange(ImageGalleryController controller, Category newCategory, Map<Long, Category> oldCategories) {
|
||||
this.controller = controller;
|
||||
this.newCategory = newCategory;
|
||||
this.oldCategories = ImmutableMap.copyOf(oldCategories);
|
||||
@ -213,8 +203,8 @@ public class CategorizeAction extends AddTagAction {
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
CategorizeAction categorizeAction = new CategorizeAction(controller);
|
||||
categorizeAction.addTagsToFiles(newCategory, "", this.oldCategories.keySet(), false);
|
||||
CategorizeAction categorizeAction = new CategorizeAction(controller, newCategory, this.oldCategories.keySet(), false);
|
||||
categorizeAction.addCatToFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -223,9 +213,10 @@ public class CategorizeAction extends AddTagAction {
|
||||
*/
|
||||
@Override
|
||||
public void undo() {
|
||||
CategorizeAction categorizeAction = new CategorizeAction(controller);
|
||||
for (Map.Entry<Long, TagName> entry : oldCategories.entrySet()) {
|
||||
categorizeAction.addTagsToFiles(entry.getValue(), "", Collections.singleton(entry.getKey()), false);
|
||||
|
||||
for (Map.Entry<Long, Category> entry : oldCategories.entrySet()) {
|
||||
new CategorizeAction(controller, entry.getValue(), Collections.singleton(entry.getKey()), false)
|
||||
.addCatToFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,21 +19,19 @@
|
||||
package org.sleuthkit.autopsy.imagegallery.actions;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.Set;
|
||||
import org.controlsfx.control.action.Action;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class CategorizeGroupAction extends Action {
|
||||
public class CategorizeGroupAction extends CategorizeAction {
|
||||
|
||||
public CategorizeGroupAction(Category cat, ImageGalleryController controller) {
|
||||
super(cat.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> {
|
||||
Set<Long> fileIdSet = ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs());
|
||||
new CategorizeAction(controller).addTagsToFiles(controller.getTagsManager().getTagName(cat), "", fileIdSet);
|
||||
});
|
||||
setGraphic(cat.getGraphic());
|
||||
super(controller, cat, null);
|
||||
setEventHandler(actionEvent ->
|
||||
new CategorizeAction(controller, cat, ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs()))
|
||||
.handle(actionEvent)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,17 +18,19 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.imagegallery.actions;
|
||||
|
||||
import org.controlsfx.control.action.Action;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class CategorizeSelectedFilesAction extends Action {
|
||||
public class CategorizeSelectedFilesAction extends CategorizeAction {
|
||||
|
||||
public CategorizeSelectedFilesAction(Category cat, ImageGalleryController controller) {
|
||||
super(cat.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(cat), ""));
|
||||
setGraphic(cat.getGraphic());
|
||||
super(controller, cat, null);
|
||||
setEventHandler(actionEvent ->
|
||||
new CategorizeAction(controller, cat, controller.getSelectionModel().getSelected())
|
||||
.handle(actionEvent)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -19,21 +19,19 @@
|
||||
package org.sleuthkit.autopsy.imagegallery.actions;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.Set;
|
||||
import org.controlsfx.control.action.Action;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.datamodel.TagName;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class TagGroupAction extends Action {
|
||||
public class TagGroupAction extends AddTagAction {
|
||||
|
||||
public TagGroupAction(final TagName tagName, ImageGalleryController controller) {
|
||||
super(tagName.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> {
|
||||
Set<Long> fileIdSet = ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs());
|
||||
new AddDrawableTagAction(controller).addTagsToFiles(tagName, "", fileIdSet);
|
||||
});
|
||||
setGraphic(controller.getTagsManager().getGraphic(tagName));
|
||||
super(controller, tagName, null);
|
||||
setEventHandler(actionEvent ->
|
||||
new AddTagAction(controller, tagName, ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs())).
|
||||
handle(actionEvent)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,17 +18,19 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.imagegallery.actions;
|
||||
|
||||
import org.controlsfx.control.action.Action;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.datamodel.TagName;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class TagSelectedFilesAction extends Action {
|
||||
public class TagSelectedFilesAction extends AddTagAction {
|
||||
|
||||
public TagSelectedFilesAction(final TagName tagName, ImageGalleryController controller) {
|
||||
super(tagName.getDisplayName(), actionEvent -> new AddDrawableTagAction(controller).addTag(tagName, ""));
|
||||
setGraphic(controller.getTagsManager().getGraphic(tagName));
|
||||
super(controller, tagName, null);
|
||||
setEventHandler(actionEvent ->
|
||||
new AddTagAction(controller, tagName, controller.getSelectionModel().getSelected()).
|
||||
handle(actionEvent)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,9 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Border;
|
||||
@ -94,6 +97,7 @@ public enum Category {
|
||||
private final String displayName;
|
||||
|
||||
private final int id;
|
||||
private Image snapshot;
|
||||
|
||||
private Category(Color color, int id, String name) {
|
||||
this.color = color;
|
||||
@ -118,11 +122,15 @@ public enum Category {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public Node getGraphic() {
|
||||
Region region = new Region();
|
||||
region.setBackground(new Background(new BackgroundFill(getColor(), CORNER_RADII_4, Insets.EMPTY)));
|
||||
region.setPrefSize(16, 16);
|
||||
region.setBorder(new Border(new BorderStroke(getColor().darker(), BorderStrokeStyle.SOLID, CORNER_RADII_4, BORDER_WIDTHS_2)));
|
||||
return region;
|
||||
synchronized public Node getGraphic() {
|
||||
if (snapshot == null) {
|
||||
Region region = new Region();
|
||||
region.setBackground(new Background(new BackgroundFill(getColor(), CORNER_RADII_4, Insets.EMPTY)));
|
||||
region.setPrefSize(16, 16);
|
||||
region.setBorder(new Border(new BorderStroke(getColor().darker(), BorderStrokeStyle.SOLID, CORNER_RADII_4, BORDER_WIDTHS_2)));
|
||||
Scene scene = new Scene(region, 16, 16, Color.TRANSPARENT);
|
||||
snapshot = region.snapshot(null, null);
|
||||
}
|
||||
return new ImageView(snapshot);
|
||||
}
|
||||
}
|
||||
|
@ -903,16 +903,11 @@ public final class DrawableDB {
|
||||
StringBuilder query = new StringBuilder("SELECT " + groupBy.attrName.toString() + ", COUNT(*) FROM drawable_files GROUP BY " + groupBy.attrName.toString()); //NON-NLS
|
||||
|
||||
String orderByClause = "";
|
||||
switch (sortBy) {
|
||||
case GROUP_BY_VALUE:
|
||||
orderByClause = " ORDER BY " + groupBy.attrName.toString(); //NON-NLS
|
||||
break;
|
||||
case FILE_COUNT:
|
||||
orderByClause = " ORDER BY COUNT(*)"; //NON-NLS
|
||||
break;
|
||||
case NONE:
|
||||
// case PRIORITY:
|
||||
break;
|
||||
|
||||
if (sortBy == GROUP_BY_VALUE) {
|
||||
orderByClause = " ORDER BY " + groupBy.attrName.toString();
|
||||
} else if (sortBy == GroupSortBy.FILE_COUNT) {
|
||||
orderByClause = " ORDER BY COUNT(*)";
|
||||
}
|
||||
|
||||
query.append(orderByClause);
|
||||
|
@ -45,13 +45,24 @@ 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}.
|
||||
*/
|
||||
@NbBundle.Messages({"DrawableTagsManager.followUp=Follow Up"})
|
||||
@NbBundle.Messages({"DrawableTagsManager.followUp=Follow Up",
|
||||
"DrawableTagsManager.bookMark=Bookmark"})
|
||||
public class DrawableTagsManager {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(DrawableTagsManager.class.getName());
|
||||
|
||||
private static final String FOLLOW_UP = Bundle.DrawableTagsManager_followUp();
|
||||
private static final String BOOKMARK = Bundle.DrawableTagsManager_bookMark();
|
||||
private static Image FOLLOW_UP_IMAGE;
|
||||
private static Image BOOKMARK_IMAGE;
|
||||
|
||||
public static String getFollowUpText() {
|
||||
return FOLLOW_UP;
|
||||
}
|
||||
|
||||
public static String getBookmarkText() {
|
||||
return BOOKMARK;
|
||||
}
|
||||
|
||||
final private Object autopsyTagsManagerLock = new Object();
|
||||
private TagsManager autopsyTagsManager;
|
||||
@ -70,6 +81,7 @@ public class DrawableTagsManager {
|
||||
* The tag name corresponding to the "built-in" tag "Follow Up"
|
||||
*/
|
||||
private TagName followUpTagName;
|
||||
private TagName bookmarkTagName;
|
||||
|
||||
public DrawableTagsManager(TagsManager autopsyTagsManager) {
|
||||
this.autopsyTagsManager = autopsyTagsManager;
|
||||
@ -141,6 +153,15 @@ public class DrawableTagsManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Object getBookmarkTagName() throws TskCoreException {
|
||||
synchronized (autopsyTagsManagerLock) {
|
||||
if (Objects.isNull(bookmarkTagName)) {
|
||||
bookmarkTagName = getTagName(BOOKMARK);
|
||||
}
|
||||
return bookmarkTagName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get all the TagNames that are not categories
|
||||
*
|
||||
@ -192,7 +213,7 @@ public class DrawableTagsManager {
|
||||
} catch (TagsManager.TagNameAlreadyExistsException ex) {
|
||||
throw new TskCoreException("tagame exists but wasn't found", ex);
|
||||
}
|
||||
} catch (IllegalStateException 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);
|
||||
}
|
||||
@ -241,9 +262,11 @@ public class DrawableTagsManager {
|
||||
try {
|
||||
if (tagname.equals(getFollowUpTagName())) {
|
||||
return new ImageView(getFollowUpImage());
|
||||
} else if (tagname.equals(getBookmarkTagName())) {
|
||||
return new ImageView(getBookmarkImage());
|
||||
}
|
||||
} catch (TskCoreException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Failed to get \"Follow Up\" 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);
|
||||
}
|
||||
@ -254,4 +277,12 @@ public class DrawableTagsManager {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -126,7 +127,7 @@ public class GroupManager {
|
||||
private volatile DrawableAttribute<?> groupBy = DrawableAttribute.PATH;
|
||||
private volatile SortOrder sortOrder = SortOrder.ASCENDING;
|
||||
|
||||
private final ReadOnlyObjectWrapper<GroupSortBy> sortByProp = new ReadOnlyObjectWrapper<>(sortBy);
|
||||
private final ReadOnlyObjectWrapper< Comparator<DrawableGroup>> sortByProp = new ReadOnlyObjectWrapper<>(sortBy);
|
||||
private final ReadOnlyObjectWrapper< DrawableAttribute<?>> groupByProp = new ReadOnlyObjectWrapper<>(groupBy);
|
||||
private final ReadOnlyObjectWrapper<SortOrder> sortOrderProp = new ReadOnlyObjectWrapper<>(sortOrder);
|
||||
|
||||
@ -274,7 +275,7 @@ public class GroupManager {
|
||||
} else if (unSeenGroups.contains(group) == false) {
|
||||
unSeenGroups.add(group);
|
||||
}
|
||||
FXCollections.sort(unSeenGroups, sortBy.getGrpComparator(sortOrder));
|
||||
FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -299,11 +300,11 @@ public class GroupManager {
|
||||
Platform.runLater(() -> {
|
||||
if (analyzedGroups.contains(group)) {
|
||||
analyzedGroups.remove(group);
|
||||
FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder));
|
||||
FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
|
||||
}
|
||||
if (unSeenGroups.contains(group)) {
|
||||
unSeenGroups.remove(group);
|
||||
FXCollections.sort(unSeenGroups, sortBy.getGrpComparator(sortOrder));
|
||||
FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -450,7 +451,7 @@ public class GroupManager {
|
||||
}
|
||||
}
|
||||
|
||||
public GroupSortBy getSortBy() {
|
||||
public Comparator<DrawableGroup> getSortBy() {
|
||||
return sortBy;
|
||||
}
|
||||
|
||||
@ -459,7 +460,7 @@ public class GroupManager {
|
||||
Platform.runLater(() -> sortByProp.set(sortBy));
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<GroupSortBy> getSortByProperty() {
|
||||
public ReadOnlyObjectProperty< Comparator<DrawableGroup>> getSortByProperty() {
|
||||
return sortByProp.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@ -523,8 +524,8 @@ public class GroupManager {
|
||||
setSortBy(sortBy);
|
||||
setSortOrder(sortOrder);
|
||||
Platform.runLater(() -> {
|
||||
FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder));
|
||||
FXCollections.sort(unSeenGroups, sortBy.getGrpComparator(sortOrder));
|
||||
FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
|
||||
FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -666,7 +667,7 @@ public class GroupManager {
|
||||
group = new DrawableGroup(groupKey, fileIDs, groupSeen);
|
||||
controller.getCategoryManager().registerListener(group);
|
||||
group.seenProperty().addListener((o, oldSeen, newSeen) -> {
|
||||
markGroupSeen(group, newSeen);
|
||||
Platform.runLater(() -> markGroupSeen(group, newSeen));
|
||||
});
|
||||
groupMap.put(groupKey, group);
|
||||
}
|
||||
@ -675,7 +676,7 @@ public class GroupManager {
|
||||
if (analyzedGroups.contains(group) == false) {
|
||||
analyzedGroups.add(group);
|
||||
if (Objects.isNull(task)) {
|
||||
FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder));
|
||||
FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
|
||||
}
|
||||
}
|
||||
markGroupSeen(group, groupSeen);
|
||||
@ -719,12 +720,12 @@ public class GroupManager {
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@NbBundle.Messages({"# {0} - groupBy attribute Name",
|
||||
"# {1} - sortBy name",
|
||||
"# {2} - sort Order",
|
||||
"ReGroupTask.displayTitle=regrouping files by {0} sorted by {1} in {2} order",
|
||||
"# {0} - groupBy attribute Name",
|
||||
"# {1} - atribute value",
|
||||
"ReGroupTask.progressUpdate=regrouping files by {0} : {1}"})
|
||||
"# {1} - sortBy name",
|
||||
"# {2} - sort Order",
|
||||
"ReGroupTask.displayTitle=regrouping files by {0} sorted by {1} in {2} order",
|
||||
"# {0} - groupBy attribute Name",
|
||||
"# {1} - atribute value",
|
||||
"ReGroupTask.progressUpdate=regrouping files by {0} : {1}"})
|
||||
private class ReGroupTask<A extends Comparable<A>> extends LoggedTask<Void> {
|
||||
|
||||
private ProgressHandle groupProgress;
|
||||
@ -735,8 +736,8 @@ public class GroupManager {
|
||||
|
||||
private final SortOrder sortOrder;
|
||||
|
||||
public ReGroupTask(DrawableAttribute<A> groupBy, GroupSortBy sortBy, SortOrder sortOrder) {
|
||||
super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.name(), sortOrder.toString()), true);
|
||||
ReGroupTask(DrawableAttribute<A> groupBy, GroupSortBy sortBy, SortOrder sortOrder) {
|
||||
super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), true);
|
||||
|
||||
this.groupBy = groupBy;
|
||||
this.sortBy = sortBy;
|
||||
@ -755,7 +756,7 @@ public class GroupManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
groupProgress = ProgressHandleFactory.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.name(), sortOrder.toString()), this);
|
||||
groupProgress = ProgressHandleFactory.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), this);
|
||||
Platform.runLater(() -> {
|
||||
analyzedGroups.clear();
|
||||
unSeenGroups.clear();
|
||||
@ -778,7 +779,7 @@ public class GroupManager {
|
||||
groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p);
|
||||
popuplateIfAnalyzed(new GroupKey<A>(groupBy, val), this);
|
||||
}
|
||||
Platform.runLater(() -> FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder)));
|
||||
Platform.runLater(() -> FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy)));
|
||||
|
||||
updateProgress(1, 1);
|
||||
return null;
|
||||
@ -793,4 +794,16 @@ public class GroupManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> Comparator<T> applySortOrder(final SortOrder sortOrder, Comparator<T> comparator) {
|
||||
switch (sortOrder) {
|
||||
case ASCENDING:
|
||||
return comparator;
|
||||
case DESCENDING:
|
||||
return comparator.reversed();
|
||||
case UNSORTED:
|
||||
default:
|
||||
return new GroupSortBy.AllEqualComparator<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,89 +18,50 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.imagegallery.datamodel.grouping;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.image.Image;
|
||||
import javax.swing.SortOrder;
|
||||
import static javax.swing.SortOrder.ASCENDING;
|
||||
import static javax.swing.SortOrder.DESCENDING;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
|
||||
|
||||
/**
|
||||
* enum of possible properties to sort groups by. This is the model for the drop
|
||||
* down in Toolbar as well as each enum value having the stategy
|
||||
* ({@link Comparator}) for sorting the groups
|
||||
* Pseudo enum of possible properties to sort groups by.
|
||||
*/
|
||||
@NbBundle.Messages({"GroupSortBy.groupSize=Group Size",
|
||||
"GroupSortBy.groupName=Group Name",
|
||||
"GroupSortBy.none=None",
|
||||
"GroupSortBy.priority=Priority"})
|
||||
public enum GroupSortBy implements ComparatorProvider {
|
||||
"GroupSortBy.groupName=Group Name",
|
||||
"GroupSortBy.none=None",
|
||||
"GroupSortBy.priority=Priority"})
|
||||
public class GroupSortBy implements Comparator<DrawableGroup> {
|
||||
|
||||
/**
|
||||
* sort the groups by the number of files in each sort the groups by the
|
||||
* number of files in each
|
||||
* sort the groups by the number of files in each
|
||||
*/
|
||||
FILE_COUNT(Bundle.GroupSortBy_groupSize(), true, "folder-open-image.png") { //NON-NLS
|
||||
@Override
|
||||
public Comparator<DrawableGroup> getGrpComparator(final SortOrder sortOrder) {
|
||||
return applySortOrder(sortOrder, Comparator.comparingInt(DrawableGroup::getSize));
|
||||
}
|
||||
public final static GroupSortBy FILE_COUNT = new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png", Comparator.comparing(DrawableGroup::getSize));
|
||||
|
||||
@Override
|
||||
public <A extends Comparable<A>> Comparator<A> getValueComparator(final DrawableAttribute<A> attr, final SortOrder sortOrder) {
|
||||
return getDefaultValueComparator(attr, sortOrder);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* sort the groups by the natural order of the grouping value ( eg group
|
||||
* them by path alphabetically )
|
||||
*/
|
||||
GROUP_BY_VALUE(Bundle.GroupSortBy_groupName(), true, "folder-rename.png") { //NON-NLS
|
||||
@Override
|
||||
public Comparator<DrawableGroup> getGrpComparator(final SortOrder sortOrder) {
|
||||
return applySortOrder(sortOrder, Comparator.comparing(t -> t.getGroupByValueDislpayName()));
|
||||
}
|
||||
public final static GroupSortBy GROUP_BY_VALUE = new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png", Comparator.comparing(DrawableGroup::getGroupByValueDislpayName));
|
||||
|
||||
@Override
|
||||
public <A extends Comparable<A>> Comparator<A> getValueComparator(final DrawableAttribute<A> attr, final SortOrder sortOrder) {
|
||||
return applySortOrder(sortOrder, Comparator.<A>naturalOrder());
|
||||
}
|
||||
},
|
||||
/**
|
||||
* don't sort the groups just use what ever order they come in (ingest
|
||||
* order)
|
||||
*/
|
||||
NONE(Bundle.GroupSortBy_none(), false, "prohibition.png") { //NON-NLS
|
||||
@Override
|
||||
public Comparator<DrawableGroup> getGrpComparator(SortOrder sortOrder) {
|
||||
return new NoOpComparator<>();
|
||||
}
|
||||
public final static GroupSortBy NONE = new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png", new AllEqualComparator<>());
|
||||
|
||||
@Override
|
||||
public <A extends Comparable<A>> Comparator<A> getValueComparator(DrawableAttribute<A> attr, final SortOrder sortOrder) {
|
||||
return new NoOpComparator<>();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* sort the groups by some priority metric to be determined and implemented
|
||||
*/
|
||||
PRIORITY(Bundle.GroupSortBy_priority(), false, "hashset_hits.png") { //NON-NLS
|
||||
@Override
|
||||
public Comparator<DrawableGroup> getGrpComparator(SortOrder sortOrder) {
|
||||
return Comparator.nullsLast(Comparator.comparingDouble(DrawableGroup::getHashHitDensity).thenComparingInt(DrawableGroup::getSize).reversed());
|
||||
}
|
||||
public final static GroupSortBy PRIORITY = new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png", Comparator.comparing(DrawableGroup::getHashHitDensity).thenComparing(Comparator.comparing(DrawableGroup::getUncategorizedCount)));
|
||||
|
||||
@Override
|
||||
public <A extends Comparable<A>> Comparator<A> getValueComparator(DrawableAttribute<A> attr, SortOrder sortOrder) {
|
||||
return getDefaultValueComparator(attr, sortOrder);
|
||||
}
|
||||
};
|
||||
@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));
|
||||
|
||||
/**
|
||||
* get a list of the values of this enum
|
||||
@ -108,8 +69,7 @@ public enum GroupSortBy implements ComparatorProvider {
|
||||
* @return
|
||||
*/
|
||||
public static ObservableList<GroupSortBy> getValues() {
|
||||
return FXCollections.observableArrayList(Arrays.asList(values()));
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
final private String displayName;
|
||||
@ -118,12 +78,12 @@ public enum GroupSortBy implements ComparatorProvider {
|
||||
|
||||
private final String imageName;
|
||||
|
||||
private final Boolean sortOrderEnabled;
|
||||
private final Comparator<DrawableGroup> delegate;
|
||||
|
||||
private GroupSortBy(String displayName, Boolean sortOrderEnabled, String imagePath) {
|
||||
private GroupSortBy(String displayName, String imagePath, Comparator<DrawableGroup> internalComparator) {
|
||||
this.displayName = displayName;
|
||||
this.sortOrderEnabled = sortOrderEnabled;
|
||||
this.imageName = imagePath;
|
||||
this.delegate = internalComparator;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
@ -139,49 +99,11 @@ public enum GroupSortBy implements ComparatorProvider {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public Boolean isSortOrderEnabled() {
|
||||
return sortOrderEnabled;
|
||||
}
|
||||
|
||||
private static <T> Comparator<T> applySortOrder(final SortOrder sortOrder, Comparator<T> comparator) {
|
||||
switch (sortOrder) {
|
||||
case ASCENDING:
|
||||
return comparator;
|
||||
case DESCENDING:
|
||||
return comparator.reversed();
|
||||
case UNSORTED:
|
||||
default:
|
||||
return new NoOpComparator<>();
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoOpComparator<A> implements Comparator<A> {
|
||||
static class AllEqualComparator<A> implements Comparator<A> {
|
||||
|
||||
@Override
|
||||
public int compare(A o1, A o2) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* * implementers of this interface must provide a method to compare
|
||||
* ({@link Comparable}) values and Groupings based on an
|
||||
* {@link DrawableAttribute} and a {@link SortOrder}
|
||||
*/
|
||||
interface ComparatorProvider {
|
||||
|
||||
<A extends Comparable<A>> Comparator<A> getValueComparator(DrawableAttribute<A> attr, SortOrder sortOrder);
|
||||
|
||||
Comparator<DrawableGroup> getGrpComparator(SortOrder sortOrder);
|
||||
|
||||
default <A extends Comparable<A>> Comparator<A> getDefaultValueComparator(DrawableAttribute<A> attr, SortOrder sortOrder) {
|
||||
return (A v1, A v2) -> {
|
||||
DrawableGroup g1 = ImageGalleryController.getDefault().getGroupManager().getGroupForKey(new GroupKey<>(attr, v1));
|
||||
DrawableGroup g2 = ImageGalleryController.getDefault().getGroupManager().getGroupForKey(new GroupKey<>(attr, v2));
|
||||
|
||||
return getGrpComparator(sortOrder).compare(g1, g2);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.ComboBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.RadioButton?>
|
||||
<?import javafx.scene.control.ToggleGroup?>
|
||||
<?import javafx.scene.control.Tooltip?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import org.controlsfx.control.SegmentedButton?>
|
||||
|
||||
<fx:root id="HBox" alignment="CENTER" spacing="5.0" type="HBox" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<Label fx:id="label" text="Sort By:">
|
||||
<labelFor>
|
||||
<ComboBox fx:id="sortByBox" />
|
||||
</labelFor>
|
||||
</Label>
|
||||
<fx:reference fx:id="sortBox" source="sortByBox" />
|
||||
<SegmentedButton>
|
||||
<buttons>
|
||||
<RadioButton fx:id="ascRadio" contentDisplay="LEFT" mnemonicParsing="false" selected="true">
|
||||
<toggleGroup>
|
||||
<ToggleGroup fx:id="orderGroup" />
|
||||
</toggleGroup>
|
||||
<graphic>
|
||||
<ImageView fitHeight="0.0" fitWidth="0.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../images/sort_ascending.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Ascending" />
|
||||
</tooltip>
|
||||
</RadioButton>
|
||||
<RadioButton fx:id="descRadio" contentDisplay="LEFT" mnemonicParsing="false" selected="false" toggleGroup="$orderGroup">
|
||||
<graphic>
|
||||
<ImageView fitHeight="0.0" fitWidth="0.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../images/sort_descending.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Descending" />
|
||||
</tooltip>
|
||||
</RadioButton>
|
||||
</buttons>
|
||||
</SegmentedButton>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</fx:root>
|
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2016 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.imagegallery.gui;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Comparator;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javax.swing.SortOrder;
|
||||
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SortChooser<X, Y extends Comparator<X>> extends HBox {
|
||||
|
||||
@FXML
|
||||
private RadioButton ascRadio;
|
||||
@FXML
|
||||
private RadioButton descRadio;
|
||||
@FXML
|
||||
private ToggleGroup orderGroup;
|
||||
@FXML
|
||||
private ComboBox<Y> sortByBox;
|
||||
|
||||
private final ObservableList<Y> comparators;
|
||||
|
||||
private final ReadOnlyObjectWrapper<SortOrder> sortOrder = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING);
|
||||
private final SimpleBooleanProperty sortOrderDisabled = new SimpleBooleanProperty(false);
|
||||
private final SimpleObjectProperty<ValueType> valueType = new SimpleObjectProperty<>(ValueType.NUMERIC);
|
||||
|
||||
public SortChooser(ObservableList<Y> comps) {
|
||||
this.comparators = comps;
|
||||
FXMLConstructor.construct(this, "SortChooser.fxml");
|
||||
}
|
||||
|
||||
@FXML
|
||||
void initialize() {
|
||||
assert ascRadio != null : "fx:id=\"ascRadio\" was not injected: check your FXML file 'Toolbar.fxml'.";
|
||||
assert descRadio != null : "fx:id=\"descRadio\" was not injected: check your FXML file 'Toolbar.fxml'.";
|
||||
assert orderGroup != null : "fx:id=\"orderGroup\" was not injected: check your FXML file 'Toolbar.fxml'.";
|
||||
assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'Toolbar.fxml'.";
|
||||
|
||||
ascRadio.getStyleClass().remove("radio-button");
|
||||
ascRadio.getStyleClass().add("toggle-button");
|
||||
descRadio.getStyleClass().remove("radio-button");
|
||||
descRadio.getStyleClass().add("toggle-button");
|
||||
|
||||
valueType.addListener((observable, oldValue, newValue) -> {
|
||||
ascRadio.setGraphic(new ImageView(newValue.getAscendingImage()));
|
||||
descRadio.setGraphic(new ImageView(newValue.getDescendingImage()));
|
||||
});
|
||||
|
||||
ascRadio.disableProperty().bind(sortOrderDisabled);
|
||||
descRadio.disableProperty().bind(sortOrderDisabled);
|
||||
ascRadio.selectedProperty().addListener(selectedToggle -> {
|
||||
sortOrder.set(orderGroup.getSelectedToggle() == ascRadio ? SortOrder.ASCENDING : SortOrder.DESCENDING);
|
||||
});
|
||||
|
||||
sortByBox.setItems(comparators);
|
||||
sortByBox.setCellFactory(listView -> new ComparatorCell());
|
||||
sortByBox.setButtonCell(new ComparatorCell());
|
||||
}
|
||||
|
||||
public ValueType getValueType() {
|
||||
return valueType.get();
|
||||
}
|
||||
|
||||
public void setValueType(ValueType type) {
|
||||
valueType.set(type);
|
||||
}
|
||||
|
||||
public SimpleObjectProperty<ValueType> valueTypeProperty() {
|
||||
return valueType;
|
||||
}
|
||||
|
||||
public void setSortOrderDisabled(boolean disabled) {
|
||||
sortOrderDisabled.set(disabled);
|
||||
}
|
||||
|
||||
public boolean isSortOrderDisabled() {
|
||||
return sortOrderDisabled.get();
|
||||
}
|
||||
|
||||
public SimpleBooleanProperty sortOrderDisabledProperty() {
|
||||
return sortOrderDisabled;
|
||||
}
|
||||
|
||||
public SortOrder getSortOrder() {
|
||||
return sortOrder.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<SortOrder> sortOrderProperty() {
|
||||
return sortOrder.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public Y getComparator() {
|
||||
return sortByBox.getSelectionModel().getSelectedItem();
|
||||
}
|
||||
|
||||
public void setComparator(Y selected) {
|
||||
sortByBox.getSelectionModel().select(selected);
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Y> comparatorProperty() {
|
||||
return sortByBox.getSelectionModel().selectedItemProperty();
|
||||
}
|
||||
|
||||
public enum ValueType {
|
||||
|
||||
LEXICOGRAPHIC("sort_asc_az.png", "sort_desc_az.png"),
|
||||
NUMERIC("sort_ascending.png", "sort_descending.png");
|
||||
|
||||
private final Image ascImage;
|
||||
private final Image descImage;
|
||||
|
||||
private ValueType(String ascImageName, String descImageName) {
|
||||
this.ascImage = new Image("/org/sleuthkit/autopsy/imagegallery/images/" + ascImageName);
|
||||
this.descImage = new Image("/org/sleuthkit/autopsy/imagegallery/images/" + descImageName);
|
||||
}
|
||||
|
||||
private Image getAscendingImage() {
|
||||
return ascImage;
|
||||
}
|
||||
|
||||
private Image getDescendingImage() {
|
||||
return descImage;
|
||||
}
|
||||
}
|
||||
|
||||
private class ComparatorCell extends ListCell<Y> {
|
||||
|
||||
@Override
|
||||
protected void updateItem(Y item, boolean empty) {
|
||||
super.updateItem(item, empty); //To change body of generated methods, choose Tools | Templates.
|
||||
|
||||
if (empty || null == item) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
try {
|
||||
String displayName = (String) item.getClass().getMethod("getDisplayName").invoke(item);
|
||||
setText(displayName);
|
||||
Image icon = (Image) item.getClass().getMethod("getIcon").invoke(item);
|
||||
setGraphic(new ImageView(icon));
|
||||
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
|
||||
// Exceptions.printStackTrace(ex);
|
||||
setText(item.toString());
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.image.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ProgressBar?>
|
||||
<?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.StackPane?>
|
||||
|
||||
<fx:root id="AnchorPane" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" type="javafx.scene.layout.AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<fx:root id="AnchorPane" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" type="javafx.scene.layout.AnchorPane" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<BorderPane minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||
<right>
|
||||
<HBox alignment="CENTER_RIGHT" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER_RIGHT">
|
||||
<HBox alignment="CENTER_RIGHT" prefHeight="-1.0" prefWidth="-1.0" spacing="5.0" BorderPane.alignment="CENTER_RIGHT">
|
||||
<children>
|
||||
<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="NEVER">
|
||||
<children>
|
||||
@ -18,7 +22,10 @@
|
||||
<Label id="fileUpdateLabel" fx:id="fileUpdateTaskLabel" alignment="CENTER" contentDisplay="CENTER" graphicTextGap="0.0" labelFor="$fileTaskProgresBar" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefWidth="-1.0" text="0 File Update Tasks" StackPane.alignment="CENTER">
|
||||
<StackPane.margin>
|
||||
<Insets left="3.0" right="3.0" />
|
||||
</StackPane.margin></Label>
|
||||
</StackPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
|
||||
</padding></Label>
|
||||
</children>
|
||||
<HBox.margin>
|
||||
<Insets />
|
||||
@ -27,10 +34,13 @@
|
||||
<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="NEVER">
|
||||
<children>
|
||||
<ProgressBar fx:id="bgTaskProgressBar" maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-1.0" prefHeight="24.0" prefWidth="-1.0" progress="0.0" StackPane.alignment="CENTER" />
|
||||
<Label fx:id="bgTaskLabel" alignment="CENTER" cache="false" contentDisplay="CENTER" disable="false" focusTraversable="false" labelFor="$uiTaskProgressBar" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" text="" StackPane.alignment="CENTER">
|
||||
<Label fx:id="bgTaskLabel" alignment="CENTER" cache="false" contentDisplay="CENTER" disable="false" focusTraversable="false" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" text="" StackPane.alignment="CENTER">
|
||||
<StackPane.margin>
|
||||
<Insets left="3.0" right="3.0" />
|
||||
</StackPane.margin></Label>
|
||||
</StackPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
|
||||
</padding></Label>
|
||||
</children>
|
||||
<HBox.margin>
|
||||
<Insets right="5.0" />
|
||||
@ -42,19 +52,6 @@
|
||||
</BorderPane.margin>
|
||||
</HBox>
|
||||
</right>
|
||||
<center>
|
||||
<HBox>
|
||||
<children>
|
||||
<Label fx:id="statusLabel" maxWidth="-Infinity" minWidth="-Infinity" wrapText="true" BorderPane.alignment="CENTER" HBox.hgrow="ALWAYS">
|
||||
<BorderPane.margin>
|
||||
<Insets left="10.0" right="10.0" />
|
||||
</BorderPane.margin>
|
||||
<HBox.margin>
|
||||
<Insets left="10.0" right="10.0" />
|
||||
</HBox.margin></Label>
|
||||
</children>
|
||||
</HBox>
|
||||
</center>
|
||||
<left><Label fx:id="staleLabel" text="Some data may be out of date. Enable listening to ingest to update." BorderPane.alignment="CENTER">
|
||||
<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2013-14 Basis Technology Corp.
|
||||
* Copyright 2013-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -19,8 +19,6 @@
|
||||
package org.sleuthkit.autopsy.imagegallery.gui;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
@ -38,21 +36,12 @@ public class StatusBar extends AnchorPane {
|
||||
|
||||
private final ImageGalleryController controller;
|
||||
|
||||
@FXML
|
||||
private ResourceBundle resources;
|
||||
|
||||
@FXML
|
||||
private URL location;
|
||||
|
||||
@FXML
|
||||
private ProgressBar fileTaskProgresBar;
|
||||
|
||||
@FXML
|
||||
private Label fileUpdateTaskLabel;
|
||||
|
||||
@FXML
|
||||
private Label statusLabel;
|
||||
|
||||
@FXML
|
||||
private Label bgTaskLabel;
|
||||
|
||||
@ -64,31 +53,24 @@ public class StatusBar extends AnchorPane {
|
||||
|
||||
@FXML
|
||||
@NbBundle.Messages({"StatusBar.fileUpdateTaskLabel.text= File Update Tasks",
|
||||
"StatusBar.bgTaskLabel.text=Regrouping",
|
||||
"StatuBar.toolTip=Some data may be out of date. Enable Image Gallery in Tools | Options | Image /Video Gallery , after ingest is complete to update the Image Gallery data."})
|
||||
"StatusBar.bgTaskLabel.text=Regrouping",
|
||||
"StatuBar.toolTip=Some data may be out of date. Enable Image Gallery in Tools | Options | Image /Video Gallery , after ingest is complete to update the Image Gallery data."})
|
||||
void initialize() {
|
||||
assert fileTaskProgresBar != null : "fx:id=\"fileTaskProgresBar\" was not injected: check your FXML file 'StatusBar.fxml'.";
|
||||
assert fileUpdateTaskLabel != null : "fx:id=\"fileUpdateTaskLabel\" was not injected: check your FXML file 'StatusBar.fxml'.";
|
||||
assert statusLabel != null : "fx:id=\"statusLabel\" was not injected: check your FXML file 'StatusBar.fxml'.";
|
||||
assert bgTaskLabel != null : "fx:id=\"bgTaskLabel\" was not injected: check your FXML file 'StatusBar.fxml'.";
|
||||
assert bgTaskProgressBar != null : "fx:id=\"bgTaskProgressBar\" was not injected: check your FXML file 'StatusBar.fxml'.";
|
||||
|
||||
fileUpdateTaskLabel.textProperty().bind(controller.getFileUpdateQueueSizeProperty().asString().concat(Bundle.StatusBar_fileUpdateTaskLabel_text()));//;setText(newSize.toString() + " File Update Tasks");
|
||||
fileTaskProgresBar.progressProperty().bind(controller.getFileUpdateQueueSizeProperty().negate());
|
||||
// controller.getFileUpdateQueueSizeProperty().addListener((ov, oldSize, newSize) -> {
|
||||
// Platform.runLater(() -> {
|
||||
//
|
||||
//
|
||||
// });
|
||||
// });
|
||||
fileUpdateTaskLabel.textProperty().bind(controller.getDBTasksQueueSizeProperty().asString().concat(Bundle.StatusBar_fileUpdateTaskLabel_text()));
|
||||
fileTaskProgresBar.progressProperty().bind(controller.getDBTasksQueueSizeProperty().negate());
|
||||
|
||||
controller.regroupProgress().addListener((ov, oldSize, newSize) -> {
|
||||
Platform.runLater(() -> {
|
||||
if(controller.regroupProgress().lessThan(1.0).get()){
|
||||
if (controller.regroupProgress().lessThan(1.0).get()) {
|
||||
// Regrouping in progress
|
||||
bgTaskProgressBar.progressProperty().setValue(-1.0);
|
||||
bgTaskLabel.setText(Bundle.StatusBar_bgTaskLabel_text());
|
||||
} else{
|
||||
} else {
|
||||
// Clear the progress bar
|
||||
bgTaskProgressBar.progressProperty().setValue(0.0);
|
||||
bgTaskLabel.setText("");
|
||||
@ -96,10 +78,7 @@ public class StatusBar extends AnchorPane {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Platform.runLater(() -> {
|
||||
staleLabel.setTooltip(new Tooltip(Bundle.StatuBar_toolTip()));
|
||||
});
|
||||
Platform.runLater(() -> staleLabel.setTooltip(new Tooltip(Bundle.StatuBar_toolTip())));
|
||||
staleLabel.visibleProperty().bind(controller.stale());
|
||||
}
|
||||
|
||||
@ -116,14 +95,4 @@ public class StatusBar extends AnchorPane {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setLabelText(final String newText) {
|
||||
Platform.runLater(() -> {
|
||||
statusLabel.setText(newText);
|
||||
});
|
||||
}
|
||||
|
||||
public String getLabeltext() {
|
||||
return statusLabel.getText();
|
||||
}
|
||||
}
|
||||
|
@ -1,113 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.ComboBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
<?import javafx.scene.control.RadioButton?>
|
||||
<?import javafx.scene.control.Separator?>
|
||||
<?import javafx.scene.control.Slider?>
|
||||
<?import javafx.scene.control.SplitMenuButton?>
|
||||
<?import javafx.scene.control.ToggleGroup?>
|
||||
<?import javafx.scene.control.ToolBar?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<fx:root minWidth="-1.0" orientation="HORIZONTAL" prefWidth="-1.0" type="javafx.scene.control.ToolBar" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<items>
|
||||
<Label fx:id="groupByLabel" text="Group By:">
|
||||
<labelFor>
|
||||
<ComboBox fx:id="groupByBox" editable="false" />
|
||||
</labelFor>
|
||||
</Label>
|
||||
<fx:reference source="groupByBox" />
|
||||
<Region prefHeight="-1.0" prefWidth="10.0" />
|
||||
<Label fx:id="sortByLabel" text="Sort By:">
|
||||
<labelFor>
|
||||
<ComboBox fx:id="sortByBox" />
|
||||
</labelFor>
|
||||
</Label>
|
||||
<HBox id="HBox" fx:id="sortControlGroup" alignment="CENTER" spacing="5.0">
|
||||
<children>
|
||||
<fx:reference source="sortByBox" />
|
||||
<VBox alignment="CENTER_LEFT" prefHeight="-1.0" prefWidth="-1.0" spacing="2.0">
|
||||
<children>
|
||||
<RadioButton fx:id="ascRadio" contentDisplay="LEFT" mnemonicParsing="false" selected="true" text="Ascending">
|
||||
<graphic>
|
||||
<ImageView fitHeight="0.0" fitWidth="0.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../images/arrow_up.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
<toggleGroup>
|
||||
<ToggleGroup fx:id="orderGroup" />
|
||||
</toggleGroup>
|
||||
</RadioButton>
|
||||
<RadioButton fx:id="descRadio" contentDisplay="LEFT" mnemonicParsing="false" selected="false" text="Descending" toggleGroup="$orderGroup">
|
||||
<graphic>
|
||||
<ImageView fitHeight="0.0" fitWidth="0.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../images/arrow_down.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</RadioButton>
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
</HBox>
|
||||
<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:">
|
||||
<graphic>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../images/tag_red.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Label>
|
||||
<SplitMenuButton id="tagSplitMenu" fx:id="tagGroupMenuButton" disable="true" mnemonicParsing="false" text="Follow Up" textOverrun="ELLIPSIS">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="Action 1" />
|
||||
<MenuItem mnemonicParsing="false" text="Action 2" />
|
||||
</items>
|
||||
</SplitMenuButton>
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER" spacing="5.0">
|
||||
<children>
|
||||
<Label fx:id="categoryImageViewLabel" text="Categorize Group's Files:">
|
||||
<graphic>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../images/category-icon.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Label>
|
||||
<SplitMenuButton id="catSplitMenu" fx:id="catGroupMenuButton" disable="true" mnemonicParsing="false" text="Cat-0">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="Action 1" />
|
||||
<MenuItem mnemonicParsing="false" text="Action 2" />
|
||||
</items>
|
||||
</SplitMenuButton>
|
||||
</children>
|
||||
</HBox>
|
||||
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" />
|
||||
<HBox alignment="CENTER" spacing="5.0">
|
||||
<children>
|
||||
<Label fx:id="thumbnailSizeLabel" text="Thumbnail Size (px):">
|
||||
<labelFor>
|
||||
<Slider fx:id="sizeSlider" blockIncrement="100.0" majorTickUnit="100.0" max="300.0" min="100.0" minorTickCount="0" orientation="HORIZONTAL" prefHeight="-1.0" showTickLabels="true" showTickMarks="true" snapToTicks="true" value="100.0" />
|
||||
</labelFor>
|
||||
</Label>
|
||||
<fx:reference source="sizeSlider" />
|
||||
</children>
|
||||
</HBox>
|
||||
</items>
|
||||
<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" />
|
||||
|
||||
</children>
|
||||
</HBox>
|
||||
|
||||
|
||||
|
||||
|
||||
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="10.0" />
|
||||
<HBox alignment="CENTER" spacing="5.0">
|
||||
<children>
|
||||
<Label fx:id="tagImageViewLabel" text="Tag Group's Files:">
|
||||
<graphic>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../images/tag_red.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Label>
|
||||
<SplitMenuButton id="tagSplitMenu" fx:id="tagGroupMenuButton" disable="true" mnemonicParsing="false" text="Follow Up" textOverrun="ELLIPSIS">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="Action 1" />
|
||||
<MenuItem mnemonicParsing="false" text="Action 2" />
|
||||
</items>
|
||||
</SplitMenuButton>
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER" spacing="5.0">
|
||||
<children>
|
||||
<Label fx:id="categoryImageViewLabel" text="Categorize Group's Files:">
|
||||
<graphic>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../images/category-icon.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Label>
|
||||
<SplitMenuButton id="catSplitMenu" fx:id="catGroupMenuButton" disable="true" mnemonicParsing="false" text="Cat-0">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="Action 1" />
|
||||
<MenuItem mnemonicParsing="false" text="Action 2" />
|
||||
</items>
|
||||
</SplitMenuButton>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets left="5.0" />
|
||||
</padding>
|
||||
</HBox>
|
||||
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="10.0" />
|
||||
<HBox alignment="CENTER" spacing="5.0">
|
||||
<children>
|
||||
<Label fx:id="thumbnailSizeLabel" text="Thumbnail Size (px):">
|
||||
<labelFor>
|
||||
<Slider fx:id="sizeSlider" blockIncrement="100.0" majorTickUnit="100.0" max="300.0" min="100.0" minorTickCount="0" orientation="HORIZONTAL" prefHeight="-1.0" showTickLabels="true" showTickMarks="true" snapToTicks="true" value="100.0" />
|
||||
</labelFor>
|
||||
</Label>
|
||||
<fx:reference source="sizeSlider" />
|
||||
</children>
|
||||
</HBox>
|
||||
</items>
|
||||
</fx:root>
|
||||
|
@ -32,14 +32,10 @@ import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.scene.control.SplitMenuButton;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.control.ToolBar;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javax.swing.SortOrder;
|
||||
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
|
||||
@ -48,6 +44,7 @@ import org.sleuthkit.autopsy.imagegallery.actions.CategorizeGroupAction;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.TagGroupAction;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
|
||||
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;
|
||||
@ -67,21 +64,6 @@ public class Toolbar extends ToolBar {
|
||||
@FXML
|
||||
private Slider sizeSlider;
|
||||
|
||||
@FXML
|
||||
private ComboBox<GroupSortBy> sortByBox;
|
||||
|
||||
@FXML
|
||||
private RadioButton ascRadio;
|
||||
|
||||
@FXML
|
||||
private RadioButton descRadio;
|
||||
|
||||
@FXML
|
||||
private ToggleGroup orderGroup;
|
||||
|
||||
@FXML
|
||||
private HBox sortControlGroup;
|
||||
|
||||
@FXML
|
||||
private SplitMenuButton catGroupMenuButton;
|
||||
|
||||
@ -91,9 +73,6 @@ public class Toolbar extends ToolBar {
|
||||
@FXML
|
||||
private Label groupByLabel;
|
||||
|
||||
@FXML
|
||||
private Label sortByLabel;
|
||||
|
||||
@FXML
|
||||
private Label tagImageViewLabel;
|
||||
|
||||
@ -107,20 +86,18 @@ public class Toolbar extends ToolBar {
|
||||
|
||||
private final SimpleObjectProperty<SortOrder> orderProperty = new SimpleObjectProperty<>(SortOrder.ASCENDING);
|
||||
|
||||
private final InvalidationListener queryInvalidationListener = (Observable o) -> {
|
||||
if (orderGroup.getSelectedToggle() == ascRadio) {
|
||||
orderProperty.set(SortOrder.ASCENDING);
|
||||
} else {
|
||||
orderProperty.set(SortOrder.DESCENDING);
|
||||
}
|
||||
|
||||
ImageGalleryController.getDefault().getGroupManager().regroup(groupByBox.getSelectionModel().getSelectedItem(), sortByBox.getSelectionModel().getSelectedItem(), getSortOrder(), false);
|
||||
};
|
||||
private final ImageGalleryController controller;
|
||||
private SortChooser<DrawableGroup, GroupSortBy> sortChooser;
|
||||
|
||||
synchronized public SortOrder getSortOrder() {
|
||||
return orderProperty.get();
|
||||
}
|
||||
private final InvalidationListener queryInvalidationListener = new InvalidationListener() {
|
||||
public void invalidated(Observable o) {
|
||||
controller.getGroupManager().regroup(
|
||||
groupByBox.getSelectionModel().getSelectedItem(),
|
||||
sortChooser.getComparator(),
|
||||
sortChooser.getSortOrder(),
|
||||
false);
|
||||
}
|
||||
};
|
||||
|
||||
public DoubleProperty sizeSliderValue() {
|
||||
return sizeSlider.valueProperty();
|
||||
@ -135,22 +112,16 @@ public class Toolbar extends ToolBar {
|
||||
|
||||
@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.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):"})
|
||||
void initialize() {
|
||||
assert ascRadio != null : "fx:id=\"ascRadio\" was not injected: check your FXML file 'Toolbar.fxml'.";
|
||||
assert catGroupMenuButton != null : "fx:id=\"catSelectedMenubutton\" was not injected: check your FXML file 'Toolbar.fxml'.";
|
||||
assert descRadio != null : "fx:id=\"descRadio\" 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 orderGroup != null : "fx:id=\"orderGroup\" 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 sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'Toolbar.fxml'.";
|
||||
assert sortControlGroup != null : "fx:id=\"sortControlGroup\" was not injected: check your FXML file 'Toolbar.fxml'.";
|
||||
assert tagGroupMenuButton != null : "fx:id=\"tagSelectedMenubutton\" was not injected: check your FXML file 'Toolbar.fxml'.";
|
||||
|
||||
controller.viewState().addListener((observable, oldViewState, newViewState) -> {
|
||||
@ -174,14 +145,6 @@ public class Toolbar extends ToolBar {
|
||||
}
|
||||
});
|
||||
|
||||
groupByLabel.setText(Bundle.Toolbar_groupByLabel());
|
||||
sortByLabel.setText(Bundle.Toolbar_sortByLabel());
|
||||
ascRadio.setText(Bundle.Toolbar_ascRadio());
|
||||
descRadio.setText(Bundle.Toolbar_descRadio());
|
||||
tagImageViewLabel.setText(Bundle.Toolbar_tagImageViewLabel());
|
||||
categoryImageViewLabel.setText(Bundle.Toolbar_categoryImageViewLabel());
|
||||
thumbnailSizeLabel.setText(Bundle.Toolbar_thumbnailSizeLabel());
|
||||
|
||||
CategorizeGroupAction cat5GroupAction = new CategorizeGroupAction(Category.FIVE, controller);
|
||||
catGroupMenuButton.setOnAction(cat5GroupAction);
|
||||
catGroupMenuButton.setText(cat5GroupAction.getText());
|
||||
@ -194,6 +157,12 @@ 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);
|
||||
@ -201,21 +170,20 @@ public class Toolbar extends ToolBar {
|
||||
groupByBox.setCellFactory(listView -> new AttributeListCell());
|
||||
groupByBox.setButtonCell(new AttributeListCell());
|
||||
|
||||
sortByBox.setCellFactory(listView -> new SortByListCell());
|
||||
sortByBox.setButtonCell(new SortByListCell());
|
||||
sortByBox.setItems(GroupSortBy.getValues());
|
||||
|
||||
sortByBox.getSelectionModel().selectedItemProperty().addListener(queryInvalidationListener);
|
||||
|
||||
sortByBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
|
||||
final boolean orderEnabled = newValue == GroupSortBy.NONE || newValue == GroupSortBy.PRIORITY;
|
||||
ascRadio.setDisable(orderEnabled);
|
||||
descRadio.setDisable(orderEnabled);
|
||||
sortChooser = new SortChooser<>(GroupSortBy.getValues());
|
||||
sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> {
|
||||
final boolean orderEnabled = newComparator == GroupSortBy.NONE || newComparator == GroupSortBy.PRIORITY;
|
||||
sortChooser.setSortOrderDisabled(orderEnabled);
|
||||
|
||||
final SortChooser.ValueType valueType = newComparator == GroupSortBy.GROUP_BY_VALUE ? SortChooser.ValueType.LEXICOGRAPHIC : SortChooser.ValueType.NUMERIC;
|
||||
sortChooser.setValueType(valueType);
|
||||
queryInvalidationListener.invalidated(observable);
|
||||
});
|
||||
sortByBox.getSelectionModel().select(GroupSortBy.PRIORITY);
|
||||
|
||||
orderGroup.selectedToggleProperty().addListener(queryInvalidationListener);
|
||||
sortChooser.sortOrderProperty().addListener(queryInvalidationListener);
|
||||
sortChooser.setComparator(GroupSortBy.PRIORITY);
|
||||
getItems().add(1, sortChooser);
|
||||
|
||||
}
|
||||
|
||||
private void syncGroupControlsEnabledState(GroupViewState newViewState) {
|
||||
@ -230,8 +198,6 @@ public class Toolbar extends ToolBar {
|
||||
public void reset() {
|
||||
Platform.runLater(() -> {
|
||||
groupByBox.getSelectionModel().select(DrawableAttribute.PATH);
|
||||
sortByBox.getSelectionModel().select(GroupSortBy.NONE);
|
||||
orderGroup.selectToggle(ascRadio);
|
||||
sizeSlider.setValue(SIZE_SLIDER_DEFAULT);
|
||||
});
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ import org.sleuthkit.autopsy.directorytree.NewWindowViewAction;
|
||||
import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.AddDrawableTagAction;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.AddTagAction;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.DeleteFollowUpTagAction;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction;
|
||||
@ -142,7 +142,7 @@ public abstract class DrawableTileBase extends DrawableUIBase {
|
||||
* @param controller the value of controller
|
||||
*/
|
||||
@NbBundle.Messages({"DrawableTileBase.menuItem.extractFiles=Extract File(s)",
|
||||
"DrawableTileBase.menuItem.showContentViewer=Show Content Viewer"})
|
||||
"DrawableTileBase.menuItem.showContentViewer=Show Content Viewer"})
|
||||
protected DrawableTileBase(GroupPane groupPane, final ImageGalleryController controller) {
|
||||
super(controller);
|
||||
this.groupPane = groupPane;
|
||||
@ -182,10 +182,8 @@ public abstract class DrawableTileBase extends DrawableUIBase {
|
||||
private ContextMenu buildContextMenu(DrawableFile<?> file) {
|
||||
final ArrayList<MenuItem> menuItems = new ArrayList<>();
|
||||
|
||||
menuItems.add(new CategorizeAction(getController()).getPopupMenu());
|
||||
|
||||
menuItems.add(new AddDrawableTagAction(getController()).getPopupMenu());
|
||||
|
||||
menuItems.add(CategorizeAction.getCategoriesMenu(getController()));
|
||||
menuItems.add(AddTagAction.getTagMenu(getController()));
|
||||
|
||||
final MenuItem extractMenuItem = new MenuItem(Bundle.DrawableTileBase_menuItem_extractFiles());
|
||||
extractMenuItem.setOnAction(actionEvent -> {
|
||||
@ -196,7 +194,6 @@ public abstract class DrawableTileBase extends DrawableUIBase {
|
||||
});
|
||||
menuItems.add(extractMenuItem);
|
||||
|
||||
|
||||
MenuItem contentViewer = new MenuItem(Bundle.DrawableTileBase_menuItem_showContentViewer());
|
||||
contentViewer.setOnAction(actionEvent -> {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
@ -242,7 +239,7 @@ public abstract class DrawableTileBase extends DrawableUIBase {
|
||||
if (followUpToggle.isSelected() == true) {
|
||||
try {
|
||||
selectionModel.clearAndSelect(file.getId());
|
||||
new AddDrawableTagAction(getController()).addTag(getController().getTagsManager().getFollowUpTagName(), "");
|
||||
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
|
||||
}
|
||||
|
@ -19,6 +19,7 @@
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -47,6 +48,7 @@ import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
@ -110,7 +112,7 @@ import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
|
||||
import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.AddDrawableTagAction;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.AddTagAction;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.Back;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction;
|
||||
import org.sleuthkit.autopsy.imagegallery.actions.CategorizeSelectedFilesAction;
|
||||
@ -377,10 +379,10 @@ public class GroupPane extends BorderPane {
|
||||
*/
|
||||
@FXML
|
||||
@NbBundle.Messages({"GroupPane.gridViewContextMenuItem.extractFiles=Extract File(s)",
|
||||
"GroupPane.bottomLabel.displayText=Group Viewing History: ",
|
||||
"GroupPane.hederLabel.displayText=Tag Selected Files:",
|
||||
"GroupPane.catContainerLabel.displayText=Categorize Selected File:",
|
||||
"GroupPane.catHeadingLabel.displayText=Category:"})
|
||||
"GroupPane.bottomLabel.displayText=Group Viewing History: ",
|
||||
"GroupPane.hederLabel.displayText=Tag Selected Files:",
|
||||
"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'.";
|
||||
@ -403,9 +405,9 @@ public class GroupPane extends BorderPane {
|
||||
toggleForCategory.getStyleClass().add("toggle-button");
|
||||
toggleForCategory.selectedProperty().addListener((ov, wasSelected, toggleSelected) -> {
|
||||
if (toggleSelected && slideShowPane != null) {
|
||||
slideShowPane.getFileID().ifPresent((fileID) -> {
|
||||
slideShowPane.getFileID().ifPresent(fileID -> {
|
||||
selectionModel.clearAndSelect(fileID);
|
||||
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(cat), "");
|
||||
new CategorizeAction(controller, cat, ImmutableSet.of(fileID)).handle(null);
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -762,37 +764,42 @@ public class GroupPane extends BorderPane {
|
||||
selectAllFiles();
|
||||
t.consume();
|
||||
}
|
||||
if (selectionModel.getSelected().isEmpty() == false) {
|
||||
switch (t.getCode()) {
|
||||
case NUMPAD0:
|
||||
case DIGIT0:
|
||||
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.ZERO), "");
|
||||
break;
|
||||
case NUMPAD1:
|
||||
case DIGIT1:
|
||||
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.ONE), "");
|
||||
break;
|
||||
case NUMPAD2:
|
||||
case DIGIT2:
|
||||
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.TWO), "");
|
||||
break;
|
||||
case NUMPAD3:
|
||||
case DIGIT3:
|
||||
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.THREE), "");
|
||||
break;
|
||||
case NUMPAD4:
|
||||
case DIGIT4:
|
||||
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.FOUR), "");
|
||||
break;
|
||||
case NUMPAD5:
|
||||
case DIGIT5:
|
||||
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.FIVE), "");
|
||||
break;
|
||||
ObservableSet<Long> selected = selectionModel.getSelected();
|
||||
if (selected.isEmpty() == false) {
|
||||
Category cat = keyCodeToCat(t.getCode());
|
||||
if (cat != null) {
|
||||
new CategorizeAction(controller, cat, selected).handle(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Category keyCodeToCat(KeyCode t) {
|
||||
if (t != null) {
|
||||
switch (t) {
|
||||
case NUMPAD0:
|
||||
case DIGIT0:
|
||||
return Category.ZERO;
|
||||
case NUMPAD1:
|
||||
case DIGIT1:
|
||||
return Category.ONE;
|
||||
case NUMPAD2:
|
||||
case DIGIT2:
|
||||
return Category.TWO;
|
||||
case NUMPAD3:
|
||||
case DIGIT3:
|
||||
return Category.THREE;
|
||||
case NUMPAD4:
|
||||
case DIGIT4:
|
||||
return Category.FOUR;
|
||||
case NUMPAD5:
|
||||
case DIGIT5:
|
||||
return Category.FIVE;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void handleArrows(KeyEvent t) {
|
||||
Long lastSelectFileId = selectionModel.lastSelectedProperty().get();
|
||||
|
||||
@ -826,8 +833,10 @@ public class GroupPane extends BorderPane {
|
||||
private ContextMenu buildContextMenu() {
|
||||
ArrayList<MenuItem> menuItems = new ArrayList<>();
|
||||
|
||||
menuItems.add(new CategorizeAction(controller).getPopupMenu());
|
||||
menuItems.add(new AddDrawableTagAction(controller).getPopupMenu());
|
||||
|
||||
menuItems.add(CategorizeAction.getCategoriesMenu(controller));
|
||||
menuItems.add(AddTagAction.getTagMenu(controller));
|
||||
|
||||
|
||||
Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
.groupTreeCell{
|
||||
.groupCell{
|
||||
|
||||
-fx-indent:5; /* default indent is 10 */
|
||||
}
|
@ -0,0 +1,306 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2016 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import java.util.Optional;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Cell;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Labeled;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
|
||||
import org.sleuthkit.datamodel.TagName;
|
||||
|
||||
/**
|
||||
* A Factory for Cells to use in a ListView<DrawableGroup> or
|
||||
* TreeView<GroupTreeNode>
|
||||
*/
|
||||
class GroupCellFactory {
|
||||
|
||||
/**
|
||||
* icon to use if a cell doesn't represent a group but just a folder(with no
|
||||
* DrawableFiles) in the file system hierarchy.
|
||||
*/
|
||||
private static final Image EMPTY_FOLDER_ICON = new Image("/org/sleuthkit/autopsy/imagegallery/images/folder.png"); //NON-NLS
|
||||
|
||||
private final ReadOnlyObjectProperty<GroupComparators<?>> sortOrder;
|
||||
private final ImageGalleryController controller;
|
||||
|
||||
GroupCellFactory(ImageGalleryController controller, ReadOnlyObjectProperty<GroupComparators<?>> sortOrderProperty) {
|
||||
this.controller = controller;
|
||||
this.sortOrder = sortOrderProperty;
|
||||
}
|
||||
|
||||
GroupListCell getListCell(ListView<DrawableGroup> listview) {
|
||||
return initCell(new GroupListCell());
|
||||
}
|
||||
|
||||
GroupTreeCell getTreeCell(TreeView<?> treeView) {
|
||||
return initCell(new GroupTreeCell());
|
||||
}
|
||||
|
||||
/**
|
||||
* remove the listener when it is not needed any more
|
||||
*
|
||||
* @param listener
|
||||
* @param oldGroup
|
||||
*/
|
||||
private void removeListeners(InvalidationListener listener, DrawableGroup oldGroup) {
|
||||
sortOrder.removeListener(listener);
|
||||
oldGroup.getFileIDs().removeListener(listener);
|
||||
oldGroup.seenProperty().removeListener(listener);
|
||||
oldGroup.uncatCountProperty().removeListener(listener);
|
||||
oldGroup.hashSetHitsCountProperty().removeListener(listener);
|
||||
}
|
||||
|
||||
private void addListeners(InvalidationListener listener, DrawableGroup group) {
|
||||
//if the sort order changes, update the counts displayed to match the sorted by property
|
||||
sortOrder.addListener(listener);
|
||||
//if number of files in this group changes (eg a file is recategorized), update counts via listener
|
||||
group.getFileIDs().addListener(listener);
|
||||
group.uncatCountProperty().addListener(listener);
|
||||
group.hashSetHitsCountProperty().addListener(listener);
|
||||
//if the seen state of this group changes update its style
|
||||
group.seenProperty().addListener(listener);
|
||||
}
|
||||
|
||||
private <X extends Cell<?> & GroupCell<?>> X initCell(X cell) {
|
||||
/*
|
||||
* reduce indent of TreeCells to 5, default is 10 which uses up a lot of
|
||||
* space. Define seen and unseen styles
|
||||
*/
|
||||
cell.getStylesheets().add(GroupCellFactory.class.getResource("GroupCell.css").toExternalForm()); //NON-NLS
|
||||
cell.getStyleClass().add("groupCell"); //NON-NLS
|
||||
|
||||
//since end of path is probably more interesting put ellipsis at front
|
||||
cell.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
|
||||
|
||||
Platform.runLater(() -> cell.prefWidthProperty().bind(cell.getView().widthProperty().subtract(15)));
|
||||
return cell;
|
||||
}
|
||||
|
||||
private <X extends Cell<?> & GroupCell<?>> void updateGroup(X cell, DrawableGroup group) {
|
||||
addListeners(cell.getGroupListener(), group);
|
||||
|
||||
//and use icon corresponding to group type
|
||||
final Node graphic = (group.getGroupByAttribute() == DrawableAttribute.TAGS)
|
||||
? controller.getTagsManager().getGraphic((TagName) group.getGroupByValue())
|
||||
: group.getGroupKey().getGraphic();
|
||||
final String text = getCellText(cell);
|
||||
final String style = getSeenStyleClass(cell);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
cell.setTooltip(new Tooltip(text));
|
||||
cell.setGraphic(graphic);
|
||||
cell.setText(text);
|
||||
cell.setStyle(style);
|
||||
});
|
||||
}
|
||||
|
||||
private <X extends Labeled & GroupCell<?>> void clearCell(X cell) {
|
||||
Platform.runLater(() -> {
|
||||
cell.setTooltip(null);
|
||||
cell.setText(null);
|
||||
cell.setGraphic(null);
|
||||
cell.setStyle("");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* return the styleClass to apply based on the assigned group's seen status
|
||||
*
|
||||
* @return the style class to apply
|
||||
*/
|
||||
private String getSeenStyleClass(GroupCell<?> cell) {
|
||||
return cell.getGroup()
|
||||
.map(DrawableGroup::isSeen)
|
||||
.map(seen -> seen ? "" : "-fx-font-weight:bold;") //NON-NLS
|
||||
.orElse(""); //if item is null or group is null
|
||||
}
|
||||
|
||||
/**
|
||||
* get the counts part of the text to apply to this cell, including
|
||||
* parentheses
|
||||
*
|
||||
* @return get the counts part of the text to apply to this cell
|
||||
*/
|
||||
private String getCountsText(GroupCell<?> cell) {
|
||||
return cell.getGroup()
|
||||
.map(group ->
|
||||
" (" + (sortOrder.get() == GroupComparators.ALPHABETICAL
|
||||
? group.getSize()
|
||||
: sortOrder.get().getFormattedValueOfGroup(group)) + ")"
|
||||
).orElse(""); //if item is null or group is null
|
||||
}
|
||||
|
||||
private String getCellText(GroupCell<?> cell) {
|
||||
return cell.getGroupName() + getCountsText(cell);
|
||||
}
|
||||
|
||||
private class GroupTreeCell extends TreeCell<GroupTreeNode> implements GroupCell<TreeView<GroupTreeNode>> {
|
||||
|
||||
private final InvalidationListener groupListener = new GroupListener<>(this);
|
||||
|
||||
/**
|
||||
* reference to group files listener that allows us to remove it from a
|
||||
* group when a new group is assigned to this Cell
|
||||
*/
|
||||
@Override
|
||||
public InvalidationListener getGroupListener() {
|
||||
return groupListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TreeView<GroupTreeNode> getView() {
|
||||
return getTreeView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return Optional.ofNullable(getItem())
|
||||
.map(treeNode -> StringUtils.defaultIfBlank(treeNode.getPath(), DrawableGroup.getBlankGroupName()))
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<DrawableGroup> getGroup() {
|
||||
return Optional.ofNullable(getItem())
|
||||
.map(GroupTreeNode::getGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void updateItem(final GroupTreeNode newItem, boolean empty) {
|
||||
//if there was a previous group, remove the listeners
|
||||
getGroup().ifPresent(oldGroup -> removeListeners(getGroupListener(), oldGroup));
|
||||
|
||||
super.updateItem(newItem, empty);
|
||||
|
||||
if (isNull(newItem) || empty) {
|
||||
clearCell(this);
|
||||
} else {
|
||||
DrawableGroup newGroup = newItem.getGroup();
|
||||
if (isNull(newGroup)) {
|
||||
//this cod epath should only be invoked for non-group Tree
|
||||
final String groupName = getGroupName();
|
||||
//"dummy" group in file system tree <=> a folder with no drawables
|
||||
Platform.runLater(() -> {
|
||||
setTooltip(new Tooltip(groupName));
|
||||
setText(groupName);
|
||||
setGraphic(new ImageView(EMPTY_FOLDER_ICON));
|
||||
setStyle("");
|
||||
});
|
||||
|
||||
} else {
|
||||
updateGroup(this, newGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class GroupListCell extends ListCell<DrawableGroup> implements GroupCell<ListView<DrawableGroup>> {
|
||||
|
||||
private final InvalidationListener groupListener = new GroupListener<>(this);
|
||||
|
||||
/**
|
||||
* reference to group files listener that allows us to remove it from a
|
||||
* group when a new group is assigned to this Cell
|
||||
*/
|
||||
@Override
|
||||
public InvalidationListener getGroupListener() {
|
||||
return groupListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListView<DrawableGroup> getView() {
|
||||
return getListView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return Optional.ofNullable(getItem())
|
||||
.map(group -> group.getGroupByValueDislpayName())
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<DrawableGroup> getGroup() {
|
||||
return Optional.ofNullable(getItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void updateItem(final DrawableGroup newGroup, boolean empty) {
|
||||
//if there was a previous group, remove the listeners
|
||||
getGroup().ifPresent(oldGroup -> removeListeners(getGroupListener(), oldGroup));
|
||||
|
||||
super.updateItem(newGroup, empty);
|
||||
|
||||
if (isNull(newGroup) || empty) {
|
||||
clearCell(this);
|
||||
} else {
|
||||
updateGroup(this, newGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface GroupCell<X extends Control> {
|
||||
|
||||
String getGroupName();
|
||||
|
||||
X getView();
|
||||
|
||||
Optional<DrawableGroup> getGroup();
|
||||
|
||||
InvalidationListener getGroupListener();
|
||||
}
|
||||
|
||||
private class GroupListener<X extends Labeled & GroupCell<?>> implements InvalidationListener {
|
||||
|
||||
private final X cell;
|
||||
|
||||
GroupListener(X cell) {
|
||||
this.cell = cell;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable o) {
|
||||
final String text = getCellText(cell);
|
||||
final String style = getSeenStyleClass(cell);
|
||||
Platform.runLater(() -> {
|
||||
cell.setText(text);
|
||||
cell.setTooltip(new Tooltip(text));
|
||||
cell.setStyle(style);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -18,21 +18,18 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@NbBundle.Messages({"GroupComparators.uncategorizedCount=Uncategorized Count",
|
||||
"GroupComparators.groupName=Group Name",
|
||||
"GroupComparators.hitCount=Hit Count",
|
||||
"GroupComparators.groupSize=Group Size",
|
||||
"GroupComparators.hitDensity=Hit Density"})
|
||||
"GroupComparators.groupName=Group Name",
|
||||
"GroupComparators.hitCount=Hit Count",
|
||||
"GroupComparators.groupSize=Group Size",
|
||||
"GroupComparators.hitDensity=Hit Density"})
|
||||
final class GroupComparators<T extends Comparable<T>> implements Comparator<DrawableGroup> {
|
||||
|
||||
static final GroupComparators<Long> UNCATEGORIZED_COUNT =
|
||||
@ -50,10 +47,10 @@ final class GroupComparators<T extends Comparable<T>> implements Comparator<Draw
|
||||
static final GroupComparators<Double> HIT_FILE_RATIO =
|
||||
new GroupComparators<>(Bundle.GroupComparators_hitDensity(), DrawableGroup::getHashHitDensity, density -> String.format("%.2f", density) + "%", true); //NON-NLS
|
||||
|
||||
private final static ImmutableList<GroupComparators<?>> values = ImmutableList.of(UNCATEGORIZED_COUNT, ALPHABETICAL, HIT_COUNT, FILE_COUNT, HIT_FILE_RATIO);
|
||||
private final static ObservableList<GroupComparators<?>> values = FXCollections.observableArrayList(UNCATEGORIZED_COUNT, ALPHABETICAL, HIT_COUNT, FILE_COUNT, HIT_FILE_RATIO);
|
||||
|
||||
public static ImmutableList<GroupComparators<?>> getValues() {
|
||||
return values;
|
||||
public static ObservableList<GroupComparators<?>> getValues() {
|
||||
return FXCollections.unmodifiableObservableList(values);
|
||||
}
|
||||
|
||||
private final Function<DrawableGroup, T> extractor;
|
||||
|
@ -1,174 +0,0 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2015-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import java.util.Optional;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javax.annotation.Nonnull;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
|
||||
import org.sleuthkit.datamodel.TagName;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class GroupListCell extends ListCell<DrawableGroup> {
|
||||
|
||||
/**
|
||||
* icon to use if this cell's TreeNode doesn't represent a group but just a
|
||||
* folder(with no DrawableFiles) in the file system hierarchy.
|
||||
*/
|
||||
private static final Image EMPTY_FOLDER_ICON =
|
||||
new Image(GroupTreeCell.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/folder.png")); //NON-NLS
|
||||
|
||||
/**
|
||||
* reference to group files listener that allows us to remove it from a
|
||||
* group when a new group is assigned to this Cell
|
||||
*/
|
||||
private final InvalidationListener fileCountListener = (Observable o) -> {
|
||||
final String text = getGroupName() + getCountsText();
|
||||
Platform.runLater(() -> {
|
||||
setText(text);
|
||||
setTooltip(new Tooltip(text));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* reference to group seen listener that allows us to remove it from a group
|
||||
* when a new group is assigned to this Cell
|
||||
*/
|
||||
private final InvalidationListener seenListener = (Observable o) -> {
|
||||
final String style = getSeenStyleClass();
|
||||
Platform.runLater(() -> setStyle(style));
|
||||
};
|
||||
|
||||
private final ReadOnlyObjectProperty<GroupComparators<?>> sortOrder;
|
||||
private final ImageGalleryController controller;
|
||||
|
||||
GroupListCell(ImageGalleryController controller, ReadOnlyObjectProperty<GroupComparators<?>> sortOrderProperty) {
|
||||
this.controller = controller;
|
||||
this.sortOrder = sortOrderProperty;
|
||||
getStylesheets().add(GroupTreeCell.class.getResource("GroupTreeCell.css").toExternalForm()); //NON-NLS
|
||||
getStyleClass().add("groupTreeCell"); //reduce indent to 5, default is 10 which uses up a lot of space. NON-NLS
|
||||
|
||||
//since end of path is probably more interesting put ellipsis at front
|
||||
setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
|
||||
Platform.runLater(() -> prefWidthProperty().bind(getListView().widthProperty().subtract(15)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void updateItem(final DrawableGroup group, boolean empty) {
|
||||
//if there was a previous group, remove the listeners
|
||||
Optional.ofNullable(getItem())
|
||||
.ifPresent(oldGroup -> {
|
||||
sortOrder.removeListener(fileCountListener);
|
||||
oldGroup.getFileIDs().removeListener(fileCountListener);
|
||||
oldGroup.seenProperty().removeListener(seenListener);
|
||||
oldGroup.uncatCountProperty().removeListener(fileCountListener);
|
||||
oldGroup.hashSetHitsCountProperty().removeListener(fileCountListener);
|
||||
});
|
||||
|
||||
super.updateItem(group, empty);
|
||||
|
||||
if (isNull(group) || empty) {
|
||||
Platform.runLater(() -> {
|
||||
setTooltip(null);
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
setStyle("");
|
||||
});
|
||||
} else {
|
||||
final String text = getGroupName() + getCountsText();
|
||||
String style;
|
||||
Node icon;
|
||||
if (isNull(group)) {
|
||||
//"dummy" group in file system tree <=> a folder with no drawables
|
||||
icon = new ImageView(EMPTY_FOLDER_ICON);
|
||||
style = "";
|
||||
} else {
|
||||
//if number of files in this group changes (eg a file is recategorized), update counts via listener
|
||||
group.getFileIDs().addListener(fileCountListener);
|
||||
group.uncatCountProperty().addListener(fileCountListener);
|
||||
group.hashSetHitsCountProperty().addListener(fileCountListener);
|
||||
sortOrder.addListener(fileCountListener);
|
||||
//if the seen state of this group changes update its style
|
||||
group.seenProperty().addListener(seenListener);
|
||||
|
||||
//and use icon corresponding to group type
|
||||
icon = (group.getGroupByAttribute() == DrawableAttribute.TAGS)
|
||||
? controller.getTagsManager().getGraphic((TagName) group.getGroupByValue())
|
||||
: group.getGroupKey().getGraphic();
|
||||
style = getSeenStyleClass();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
setTooltip(new Tooltip(text));
|
||||
setGraphic(icon);
|
||||
setText(text);
|
||||
setStyle(style);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private String getGroupName() {
|
||||
return Optional.ofNullable(getItem())
|
||||
.map(group -> group.getGroupByValueDislpayName())
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
/**
|
||||
* return the styleClass to apply based on the assigned group's seen status
|
||||
*
|
||||
* @return the style class to apply
|
||||
*/
|
||||
@Nonnull
|
||||
private String getSeenStyleClass() {
|
||||
return Optional.ofNullable(getItem())
|
||||
.map(DrawableGroup::isSeen)
|
||||
.map(seen -> seen ? "" : "-fx-font-weight:bold;") //NON-NLS
|
||||
.orElse(""); //if item is null or group is null
|
||||
}
|
||||
|
||||
/**
|
||||
* get the counts part of the text to apply to this cell, including
|
||||
* parentheses
|
||||
*
|
||||
* @return get the counts part of the text to apply to this cell
|
||||
*/
|
||||
@Nonnull
|
||||
private String getCountsText() {
|
||||
return Optional.ofNullable(getItem())
|
||||
.map(group ->
|
||||
" (" + (sortOrder.get() == GroupComparators.ALPHABETICAL
|
||||
? group.getSize()
|
||||
: sortOrder.get().getFormattedValueOfGroup(group)) + ")"
|
||||
).orElse(""); //if item is null or group is null
|
||||
}
|
||||
}
|
@ -72,7 +72,8 @@ final public class GroupTree extends NavPanel<TreeItem<GroupTreeNode>> {
|
||||
getToolBar().visibleProperty().bind(groupedByPath.not());
|
||||
getToolBar().managedProperty().bind(groupedByPath.not());
|
||||
|
||||
groupTree.setCellFactory(treeView -> new GroupTreeCell(getController(), getSortByBox().getSelectionModel().selectedItemProperty()));
|
||||
GroupCellFactory groupCellFactory = new GroupCellFactory(getController(), comparatorProperty());
|
||||
groupTree.setCellFactory(groupCellFactory::getTreeCell);
|
||||
groupTree.setShowRoot(false);
|
||||
|
||||
getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change<? extends DrawableGroup> change) -> {
|
||||
@ -151,4 +152,5 @@ final public class GroupTree extends NavPanel<TreeItem<GroupTreeNode>> {
|
||||
return Arrays.asList(stripStart);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2013-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import java.util.Optional;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javax.annotation.Nonnull;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
|
||||
import org.sleuthkit.datamodel.TagName;
|
||||
|
||||
/**
|
||||
* A cell in the NavPanel tree that listens to its associated group's fileids
|
||||
* and seen status,and updates GUI to reflect them.
|
||||
*
|
||||
* TODO: we should use getStyleClass().add() rather than setStyle but it didn't
|
||||
* seem to work properly
|
||||
*/
|
||||
class GroupTreeCell extends TreeCell<GroupTreeNode> {
|
||||
|
||||
/**
|
||||
* icon to use if this cell's TreeNode doesn't represent a group but just a
|
||||
* folder(with no DrawableFiles) in the file system hierarchy.
|
||||
*/
|
||||
private static final Image EMPTY_FOLDER_ICON =
|
||||
new Image(GroupTreeCell.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/folder.png")); //NON-NLS
|
||||
|
||||
/**
|
||||
* reference to group files listener that allows us to remove it from a
|
||||
* group when a new group is assigned to this Cell
|
||||
*/
|
||||
private final InvalidationListener fileCountListener = (Observable o) -> {
|
||||
final String text = getGroupName() + getCountsText();
|
||||
Platform.runLater(() -> {
|
||||
setText(text);
|
||||
setTooltip(new Tooltip(text));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* reference to group seen listener that allows us to remove it from a group
|
||||
* when a new group is assigned to this Cell
|
||||
*/
|
||||
private final InvalidationListener seenListener = (Observable o) -> {
|
||||
final String style = getSeenStyleClass();
|
||||
Platform.runLater(() -> {
|
||||
setStyle(style);
|
||||
});
|
||||
};
|
||||
private final ReadOnlyObjectProperty<GroupComparators<?>> sortOrder;
|
||||
private final ImageGalleryController controller;
|
||||
|
||||
GroupTreeCell(ImageGalleryController controller, ReadOnlyObjectProperty<GroupComparators<?>> sortOrderProperty) {
|
||||
this.controller = controller;
|
||||
this.sortOrder = sortOrderProperty;
|
||||
getStylesheets().add(GroupTreeCell.class.getResource("GroupTreeCell.css").toExternalForm()); //NON-NLS
|
||||
getStyleClass().add("groupTreeCell"); //reduce indent to 5, default is 10 which uses up a lot of space. NON-NLS
|
||||
|
||||
//since end of path is probably more interesting put ellipsis at front
|
||||
setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
|
||||
Platform.runLater(() -> {
|
||||
prefWidthProperty().bind(getTreeView().widthProperty().subtract(15));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc }
|
||||
*/
|
||||
@Override
|
||||
protected synchronized void updateItem(final GroupTreeNode treeNode, boolean empty) {
|
||||
//if there was a previous group, remove the listeners
|
||||
Optional.ofNullable(getItem())
|
||||
.map(GroupTreeNode::getGroup)
|
||||
.ifPresent(group -> {
|
||||
sortOrder.addListener(fileCountListener);
|
||||
group.getFileIDs().removeListener(fileCountListener);
|
||||
group.hashSetHitsCountProperty().removeListener(fileCountListener);
|
||||
group.seenProperty().removeListener(seenListener);
|
||||
group.uncatCountProperty().removeListener(fileCountListener);
|
||||
});
|
||||
|
||||
super.updateItem(treeNode, empty);
|
||||
|
||||
if (isNull(treeNode) || empty) {
|
||||
Platform.runLater(() -> {
|
||||
setTooltip(null);
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
setStyle("");
|
||||
});
|
||||
} else {
|
||||
DrawableGroup group = treeNode.getGroup();
|
||||
if (isNull(group)) {
|
||||
final String text = getGroupName();
|
||||
//"dummy" group in file system tree <=> a folder with no drawables
|
||||
Platform.runLater(() -> {
|
||||
setTooltip(new Tooltip(text));
|
||||
setText(text);
|
||||
setGraphic(new ImageView(EMPTY_FOLDER_ICON));
|
||||
setStyle("");
|
||||
});
|
||||
|
||||
} else {
|
||||
//if number of files in this group changes (eg a file is recategorized), update counts via listener
|
||||
group.getFileIDs().addListener(fileCountListener);
|
||||
group.uncatCountProperty().addListener(fileCountListener);
|
||||
group.hashSetHitsCountProperty().addListener(fileCountListener);
|
||||
sortOrder.addListener(fileCountListener);
|
||||
//if the seen state of this group changes update its style
|
||||
group.seenProperty().addListener(seenListener);
|
||||
|
||||
//and use icon corresponding to group type
|
||||
Node icon = (group.getGroupByAttribute() == DrawableAttribute.TAGS)
|
||||
? controller.getTagsManager().getGraphic((TagName) group.getGroupByValue())
|
||||
: group.getGroupKey().getGraphic();
|
||||
final String text = getGroupName() + getCountsText();
|
||||
final String style = getSeenStyleClass();
|
||||
Platform.runLater(() -> {
|
||||
setTooltip(new Tooltip(text));
|
||||
setGraphic(icon);
|
||||
setText(text);
|
||||
setStyle(style);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getGroupName() {
|
||||
return Optional.ofNullable(getItem())
|
||||
.map(treeNode -> StringUtils.defaultIfBlank(treeNode.getPath(), DrawableGroup.getBlankGroupName()))
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
/**
|
||||
* return the styleClass to apply based on the assigned group's seen status
|
||||
*
|
||||
* @return the style class to apply
|
||||
*/
|
||||
@Nonnull
|
||||
private String getSeenStyleClass() {
|
||||
return Optional.ofNullable(getItem())
|
||||
.map(GroupTreeNode::getGroup)
|
||||
.map(DrawableGroup::isSeen)
|
||||
.map(seen -> seen ? "" : "-fx-font-weight:bold;") //NON-NLS
|
||||
.orElse(""); //if item is null or group is null
|
||||
}
|
||||
|
||||
/**
|
||||
* get the counts part of the text to apply to this cell, including
|
||||
* parentheses
|
||||
*
|
||||
* @return get the counts part of the text to apply to this cell
|
||||
*/
|
||||
@Nonnull
|
||||
private String getCountsText() {
|
||||
return Optional.ofNullable(getItem())
|
||||
.map(GroupTreeNode::getGroup)
|
||||
.map(group ->
|
||||
" (" + (sortOrder.get() == GroupComparators.ALPHABETICAL
|
||||
? group.getSize()
|
||||
: sortOrder.get().getFormattedValueOfGroup(group)) + ")"
|
||||
).orElse(""); //if item is null or group is null
|
||||
}
|
||||
}
|
@ -79,8 +79,8 @@ final public class HashHitGroupList extends NavPanel<DrawableGroup> {
|
||||
getBorderPane().setCenter(groupList);
|
||||
sorted = getController().getGroupManager().getAnalyzedGroups().filtered((DrawableGroup t) -> t.getHashSetHitsCount() > 0).sorted(getDefaultComparator());
|
||||
|
||||
groupList.setCellFactory(treeView -> new GroupListCell(getController(), getSortByBox().getSelectionModel().selectedItemProperty()));
|
||||
|
||||
GroupCellFactory groupCellFactory = new GroupCellFactory(getController(), comparatorProperty());
|
||||
groupList.setCellFactory(groupCellFactory::getListCell);
|
||||
groupList.setItems(sorted);
|
||||
}
|
||||
|
||||
|
@ -1,63 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.collections.FXCollections?>
|
||||
<?import javafx.scene.control.ComboBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.Tab?>
|
||||
<?import javafx.scene.control.RadioButton?>
|
||||
<?import javafx.scene.control.ToggleGroup?>
|
||||
<?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.VBox?>
|
||||
|
||||
|
||||
<fx:root type="Tab" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" closable="false" >
|
||||
<fx:root closable="false" type="Tab" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<content>
|
||||
<BorderPane prefHeight="200.0" prefWidth="200.0" fx:id="borderPane">
|
||||
<BorderPane fx:id="borderPane">
|
||||
<top>
|
||||
<ToolBar fx:id="toolBar" BorderPane.alignment="CENTER">
|
||||
<items>
|
||||
<Label fx:id="sortByBoxLabel" text="Sort By:" />
|
||||
<ComboBox fx:id="sortByBox" maxWidth="-Infinity" minWidth="-Infinity">
|
||||
<items>
|
||||
<FXCollections fx:factory="observableArrayList">
|
||||
<String fx:value="Item 1" />
|
||||
<String fx:value="Item 2" />
|
||||
<String fx:value="Item 3" />
|
||||
</FXCollections>
|
||||
</items>
|
||||
</ComboBox>
|
||||
<VBox alignment="CENTER_LEFT" prefHeight="-1.0" prefWidth="-1.0" spacing="2.0">
|
||||
<children>
|
||||
<RadioButton fx:id="ascRadio" contentDisplay="LEFT" mnemonicParsing="false" selected="true" text="Ascending">
|
||||
<graphic>
|
||||
<ImageView fitHeight="0.0" fitWidth="0.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/arrow_up.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
<toggleGroup>
|
||||
<ToggleGroup fx:id="orderGroup" />
|
||||
</toggleGroup>
|
||||
</RadioButton>
|
||||
<RadioButton fx:id="descRadio" contentDisplay="LEFT" mnemonicParsing="false" selected="false" text="Descending" toggleGroup="$orderGroup">
|
||||
<graphic>
|
||||
<ImageView fitHeight="0.0" fitWidth="0.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/arrow_down.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</RadioButton>
|
||||
</children>
|
||||
</VBox>
|
||||
</items>
|
||||
</ToolBar>
|
||||
<ToolBar fx:id="toolBar" minWidth="-Infinity" BorderPane.alignment="CENTER" />
|
||||
</top>
|
||||
</BorderPane>
|
||||
</content>
|
||||
|
@ -22,15 +22,13 @@ import com.google.common.eventbus.Subscribe;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.SelectionModel;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.control.ToolBar;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javax.swing.SortOrder;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
|
||||
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
|
||||
@ -38,6 +36,7 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
|
||||
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
|
||||
import org.sleuthkit.autopsy.imagegallery.gui.SortChooser;
|
||||
|
||||
/**
|
||||
* Base class for Tabs in the left hand Navigation/Context area.
|
||||
@ -50,24 +49,10 @@ abstract class NavPanel<X> extends Tab {
|
||||
@FXML
|
||||
private ToolBar toolBar;
|
||||
|
||||
@FXML
|
||||
private ComboBox<GroupComparators<?>> sortByBox;
|
||||
|
||||
@FXML
|
||||
private RadioButton ascRadio;
|
||||
|
||||
@FXML
|
||||
private ToggleGroup orderGroup;
|
||||
|
||||
@FXML
|
||||
private RadioButton descRadio;
|
||||
|
||||
@FXML
|
||||
private Label sortByBoxLabel;
|
||||
|
||||
private final ImageGalleryController controller;
|
||||
private final GroupManager groupManager;
|
||||
private final CategoryManager categoryManager;
|
||||
private SortChooser<DrawableGroup, GroupComparators<?>> sortChooser;
|
||||
|
||||
NavPanel(ImageGalleryController controller) {
|
||||
this.controller = controller;
|
||||
@ -75,34 +60,35 @@ abstract class NavPanel<X> extends Tab {
|
||||
this.categoryManager = controller.getCategoryManager();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<GroupComparators<?>> comparatorProperty() {
|
||||
return sortChooser.comparatorProperty();
|
||||
}
|
||||
|
||||
@FXML
|
||||
@NbBundle.Messages({"NavPanel.ascRadio.text=Ascending",
|
||||
"NavPanel.descRadio.text=Descending",
|
||||
"NavPanel.sortByBoxLabel.text=Sort By:"})
|
||||
"NavPanel.descRadio.text=Descending",
|
||||
"NavPanel.sortByBoxLabel.text=Sort By:"})
|
||||
void initialize() {
|
||||
assert borderPane != null : "fx:id=\"borderPane\" was not injected: check your FXML file 'NavPanel.fxml'.";
|
||||
assert toolBar != null : "fx:id=\"toolBar\" was not injected: check your FXML file 'NavPanel.fxml'.";
|
||||
assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'NavPanel.fxml'.";
|
||||
assert ascRadio != null : "fx:id=\"ascRadio\" was not injected: check your FXML file 'NavPanel.fxml'.";
|
||||
assert orderGroup != null : "fx:id=\"orderGroup\" was not injected: check your FXML file 'NavPanel.fxml'.";
|
||||
assert descRadio != null : "fx:id=\"descRadio\" was not injected: check your FXML file 'NavPanel.fxml'.";
|
||||
|
||||
sortByBox.getItems().setAll(GroupComparators.getValues());
|
||||
sortByBox.getSelectionModel().select(getDefaultComparator());
|
||||
orderGroup.selectedToggleProperty().addListener(order -> sortGroups());
|
||||
sortByBox.getSelectionModel().selectedItemProperty().addListener(observable -> {
|
||||
sortChooser = new SortChooser<>(GroupComparators.getValues());
|
||||
sortChooser.setComparator(getDefaultComparator());
|
||||
sortChooser.sortOrderProperty().addListener(order -> sortGroups());
|
||||
sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> {
|
||||
sortGroups();
|
||||
//only need to listen to changes in category if we are sorting by/ showing the uncategorized count
|
||||
if (sortByBox.getSelectionModel().getSelectedItem() == GroupComparators.UNCATEGORIZED_COUNT) {
|
||||
if (newComparator == GroupComparators.UNCATEGORIZED_COUNT) {
|
||||
categoryManager.registerListener(NavPanel.this);
|
||||
} else {
|
||||
categoryManager.unregisterListener(NavPanel.this);
|
||||
}
|
||||
});
|
||||
|
||||
ascRadio.setText(Bundle.NavPanel_ascRadio_text());
|
||||
descRadio.setText(Bundle.NavPanel_descRadio_text());
|
||||
sortByBoxLabel.setText(Bundle.NavPanel_sortByBoxLabel_text());
|
||||
final SortChooser.ValueType valueType = newComparator == GroupComparators.ALPHABETICAL ? SortChooser.ValueType.LEXICOGRAPHIC : SortChooser.ValueType.NUMERIC;
|
||||
sortChooser.setValueType(valueType);
|
||||
});
|
||||
toolBar.getItems().add(sortChooser);
|
||||
|
||||
//keep selection in sync with controller
|
||||
controller.viewState().addListener(observable -> {
|
||||
Optional.ofNullable(controller.viewState().get())
|
||||
@ -129,8 +115,8 @@ abstract class NavPanel<X> extends Tab {
|
||||
*/
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
Comparator<DrawableGroup> getComparator() {
|
||||
Comparator<DrawableGroup> comparator = sortByBox.getSelectionModel().getSelectedItem();
|
||||
return (orderGroup.getSelectedToggle() == ascRadio)
|
||||
Comparator<DrawableGroup> comparator = sortChooser.getComparator();
|
||||
return (sortChooser.getSortOrder() == SortOrder.ASCENDING)
|
||||
? comparator
|
||||
: comparator.reversed();
|
||||
}
|
||||
@ -194,22 +180,6 @@ abstract class NavPanel<X> extends Tab {
|
||||
return toolBar;
|
||||
}
|
||||
|
||||
ComboBox<GroupComparators<?>> getSortByBox() {
|
||||
return sortByBox;
|
||||
}
|
||||
|
||||
RadioButton getAscRadio() {
|
||||
return ascRadio;
|
||||
}
|
||||
|
||||
ToggleGroup getOrderGroup() {
|
||||
return orderGroup;
|
||||
}
|
||||
|
||||
RadioButton getDescRadio() {
|
||||
return descRadio;
|
||||
}
|
||||
|
||||
ImageGalleryController getController() {
|
||||
return controller;
|
||||
}
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 699 B |
Binary file not shown.
After Width: | Height: | Size: 768 B |
Binary file not shown.
After Width: | Height: | Size: 714 B |
Binary file not shown.
After Width: | Height: | Size: 707 B |
Loading…
x
Reference in New Issue
Block a user