Merge remote-tracking branch 'refs/remotes/sleuthkit/develop' into develop

This commit is contained in:
Chillli 2016-02-25 15:02:49 +01:00
commit a2312c92e0
40 changed files with 1491 additions and 1596 deletions

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2011 Basis Technology Corp. * Copyright 2011-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org * Contact: carrier <at> sleuthkit <dot> org
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -95,17 +95,19 @@ class ThumbnailViewNode extends FilterNode {
super.done(); super.done();
try { try {
iconCache = new SoftReference<>(super.get()); iconCache = new SoftReference<>(super.get());
progressHandle.finish();
fireIconChange(); 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) { if (timer != null) {
timer.stop(); timer.stop();
timer = null; 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(); swingWorker.execute();
} }

View File

@ -21,7 +21,3 @@ PlatformUtil.getAllMemUsageInfo.usageText={0}\n\
{1}\n\ {1}\n\
Process Virtual Memory\: {2} Process Virtual Memory\: {2}
StringExtract.illegalStateException.cannotInit.msg=Unicode table not properly initialized, cannot instantiate StringExtract 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}

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2012-15 Basis Technology Corp. * Copyright 2012-16 Basis Technology Corp.
* *
* Copyright 2012 42six Solutions. * Copyright 2012 42six Solutions.
* Contact: aebadirad <at> 42six <dot> com * Contact: aebadirad <at> 42six <dot> com
@ -30,6 +30,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
@ -74,10 +75,6 @@ public class ImageUtils {
private static final Logger LOGGER = Logger.getLogger(ImageUtils.class.getName()); 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 * save thumbnails to disk as this format
*/ */
@ -594,7 +591,7 @@ public class ImageUtils {
try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) {
try (ImageInputStream input = ImageIO.createImageInputStream(inputStream)) { try (ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
if (input == null) { 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)); LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
throw iioException; throw iioException;
} }
@ -613,7 +610,7 @@ public class ImageUtils {
reader.dispose(); reader.dispose();
} }
} else { } 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)); LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
throw iioException; throw iioException;
@ -646,18 +643,19 @@ public class ImageUtils {
*/ */
static private class GetThumbnailTask extends ReadImageTaskBase { 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 int iconSize;
private final File cacheFile; private final File cacheFile;
private final boolean defaultOnFailure; private final boolean defaultOnFailure;
// @NbBundle.Messages({"# {0} - file name", @NbBundle.Messages({"# {0} - file name",
// "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}", "# {0} - file name", "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}",
// "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"}) "# {0} - file name",
"GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
private GetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) { private GetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) {
super(file); super(file);
updateMessage(NbBundle.getMessage(this.getClass(), "ImageUtils.GetOrGenerateThumbnailTask.loadingThumbnailFor", file.getName())); updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName()));
this.iconSize = iconSize; this.iconSize = iconSize;
this.defaultOnFailure = defaultOnFailure; this.defaultOnFailure = defaultOnFailure;
this.cacheFile = getCachedThumbnailLocation(file.getId()); this.cacheFile = getCachedThumbnailLocation(file.getId());
@ -678,36 +676,39 @@ public class ImageUtils {
if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) { if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) {
return SwingFXUtils.toFXImage(cachedThumbnail, null); return SwingFXUtils.toFXImage(cachedThumbnail, null);
} }
} catch (IOException ex) { } catch (Exception ex) {
LOGGER.log(Level.WARNING, "ImageIO had a problem reading thumbnail for image {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS 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()) { if (isCancelled()) {
return null; return null;
} }
//There was no correctly-sized cached thumbnail so make one. //There was no correctly-sized cached thumbnail so make one.
BufferedImage thumbnail = null; BufferedImage thumbnail = null;
if (VideoUtils.isVideoThumbnailSupported(file)) { if (VideoUtils.isVideoThumbnailSupported(file)) {
if (openCVLoaded) { if (openCVLoaded) {
updateMessage(NbBundle.getMessage(this.getClass(), "ImageUtils.GetOrGenerateThumbnailTask.generatingPreviewFor", file.getName())); updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize); thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize);
} }
if (null == thumbnail) { if (null == thumbnail) {
if (defaultOnFailure) { if (defaultOnFailure) {
thumbnail = DEFAULT_THUMBNAIL; thumbnail = DEFAULT_THUMBNAIL;
} else { } else {
throw new IIOException("Failed to generate thumbnail for video file."); throw new IIOException("Failed to generate a thumbnail for " + getContentPathSafe(file));
} }
} }
} else { } else {
//read the image into a buffered image. //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); BufferedImage bufferedImage = SwingFXUtils.fromFXImage(readImage(), null);
if (null == bufferedImage) { if (null == bufferedImage) {
LOGGER.log(Level.WARNING, FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION); String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file));
throw new IIOException(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION); LOGGER.log(Level.WARNING, msg);
throw new IIOException(msg);
} }
updateProgress(-1, 1); updateProgress(-1, 1);
@ -716,23 +717,21 @@ public class ImageUtils {
thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize); thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize);
} catch (IllegalArgumentException | OutOfMemoryError e) { } catch (IllegalArgumentException | OutOfMemoryError e) {
// if resizing does not work due to extreme aspect ratio or oom, crop the image instead. // 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 height = bufferedImage.getHeight();
final int width = bufferedImage.getWidth(); final int width = bufferedImage.getWidth();
if (iconSize < height || iconSize < width) { if (iconSize < height || iconSize < width) {
final int cropHeight = Math.min(iconSize, height); final int cropHeight = Math.min(iconSize, height);
final int cropWidth = Math.min(iconSize, width); final int cropWidth = Math.min(iconSize, width);
try { try {
thumbnail = ScalrWrapper.cropImage(bufferedImage, cropWidth, cropHeight); thumbnail = ScalrWrapper.cropImage(bufferedImage, cropWidth, cropHeight);
} catch (Exception cropException) { } catch (Exception cropException) {
LOGGER.log(Level.WARNING, "Could not crop image {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS LOGGER.log(Level.WARNING, "Could not crop {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS
throw cropException;
} }
} }
} catch (Exception e) { } 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; throw e;
} }
} }
@ -744,7 +743,7 @@ public class ImageUtils {
updateProgress(-1, 1); updateProgress(-1, 1);
//if we got a valid thumbnail save it //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); saveThumbnail(thumbnail);
} }
@ -790,16 +789,16 @@ public class ImageUtils {
/** /**
* A task that reads the content of a AbstractFile as a javafx Image. * 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 { static private class ReadImageTask extends ReadImageTaskBase {
ReadImageTask(AbstractFile file) { ReadImageTask(AbstractFile file) {
super(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 @Override
protected javafx.scene.image.Image call() throws Exception { protected javafx.scene.image.Image call() throws Exception {
return readImage(); return readImage();
@ -811,19 +810,18 @@ public class ImageUtils {
*/ */
static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener { 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; final AbstractFile file;
private ImageReader reader; // private ImageReader reader;
ReadImageTaskBase(AbstractFile file) { ReadImageTaskBase(AbstractFile file) {
this.file = file; this.file = file;
} }
protected javafx.scene.image.Image readImage() throws IOException { protected javafx.scene.image.Image readImage() throws IOException {
try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) {
if (ImageUtils.isGIF(file)) { if (ImageUtils.isGIF(file)) {
//use JavaFX to directly read GIF to preserve potential animation, //use JavaFX to directly read GIF to preserve potential animation
javafx.scene.image.Image image = new javafx.scene.image.Image(new BufferedInputStream(inputStream)); javafx.scene.image.Image image = new javafx.scene.image.Image(new BufferedInputStream(new ReadContentInputStream(file)));
if (image.isError() == false) { if (image.isError() == false) {
return image; return image;
} }
@ -832,49 +830,35 @@ public class ImageUtils {
if (isCancelled()) { if (isCancelled()) {
return null; 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);
//we use the first ImageReader, is there any point to trying the others? return getImageProperty(file, "ImageIO could not read {0}: ",
if (readers.hasNext()) { imageReader -> {
reader = readers.next(); imageReader.addIIOReadProgressListener(ReadImageTaskBase.this);
reader.addIIOReadProgressListener(this);
reader.setInput(input);
/* /*
* This is the important part, get or create a * This is the important part, get or create a
* ImageReadParam, create a destination image to hold * ImageReadParam, create a destination image to hold
* the decoded result, then pass that image with the * the decoded result, then pass that image with the
* param. * param.
*/ */
ImageReadParam param = reader.getDefaultReadParam(); ImageReadParam param = imageReader.getDefaultReadParam();
BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
BufferedImage bufferedImage = reader.getImageTypes(0).next().createBufferedImage(reader.getWidth(0), reader.getHeight(0));
param.setDestination(bufferedImage); param.setDestination(bufferedImage);
try { 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) { } catch (IOException iOException) {
// Ignore this exception or display a warning or similar, for exceptions happening during decoding LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + iOException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N
LOGGER.log(Level.WARNING, IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + iOException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N
} finally { } finally {
reader.removeIIOReadProgressListener(this); imageReader.removeIIOReadProgressListener(ReadImageTaskBase.this);
reader.dispose();
} }
if (isCancelled()) { if (isCancelled()) {
return null; return null;
} }
return SwingFXUtils.toFXImage(bufferedImage, null); return SwingFXUtils.toFXImage(bufferedImage, null);
} else { });
throw new IIOException(NO_IMAGE_READER_FOUND_FOR_ + ImageUtils.getContentPathSafe(file));
}
}
}
} }
@Override @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 //update this task with the progress reported by ImageReader.read
updateProgress(percentageDone, 100); updateProgress(percentageDone, 100);
if (isCancelled()) { if (isCancelled()) {
@ -890,11 +874,11 @@ public class ImageUtils {
try { try {
javafx.scene.image.Image fxImage = get(); javafx.scene.image.Image fxImage = get();
if (fxImage == null) { 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 { } else {
if (fxImage.isError()) { if (fxImage.isError()) {
//if there was somekind of error, log it //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) { } catch (InterruptedException | ExecutionException ex) {
@ -905,7 +889,7 @@ public class ImageUtils {
@Override @Override
protected void failed() { protected void failed() {
super.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 @Override
@ -950,7 +934,7 @@ public class ImageUtils {
* *
* @return * @return
*/ */
private static String getContentPathSafe(Content content) { static String getContentPathSafe(Content content) {
try { try {
return content.getUniquePath(); return content.getUniquePath();
} catch (TskCoreException tskCoreException) { } catch (TskCoreException tskCoreException) {

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2015 Basis Technology Corp. * Copyright 2015-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org * Contact: carrier <at> sleuthkit <dot> org
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -18,6 +18,7 @@
*/ */
package org.sleuthkit.autopsy.coreutils; package org.sleuthkit.autopsy.coreutils;
import com.google.common.io.Files;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -91,33 +92,33 @@ public class VideoUtils {
return isMediaThumbnailSupported(file, SUPPORTED_VIDEO_MIME_TYPES, SUPPORTED_VIDEO_EXTENSIONS, CONDITIONAL_MIME_TYPES); 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) { static BufferedImage generateVideoThumbnail(AbstractFile file, int iconSize) {
java.io.File tempFile = getTempVideoFile(file); java.io.File tempFile = getTempVideoFile(file);
try {
if (tempFile.exists() == false || tempFile.length() < file.getSize()) { if (tempFile.exists() == false || tempFile.length() < file.getSize()) {
com.google.common.io.Files.createParentDirs(tempFile); ProgressHandle progress = ProgressHandleFactory.createHandle(Bundle.VideoUtils_genVideoThumb_progress_text(file.getName()));
ProgressHandle progress = ProgressHandleFactory.createHandle(NbBundle.getMessage(VideoUtils.class, "VideoUtils.genVideoThumb.progress.text", file.getName()));
progress.start(100); progress.start(100);
try { try {
Files.createParentDirs(tempFile);
ContentUtils.writeToFile(file, tempFile, progress, null, true); ContentUtils.writeToFile(file, tempFile, progress, null, true);
} catch (IOException ex) { } catch (IOException ex) {
LOGGER.log(Level.WARNING, "Error buffering file", ex); //NON-NLS LOGGER.log(Level.WARNING, "Error extracting temporary file for " + ImageUtils.getContentPathSafe(file), ex); //NON-NLS
} } finally {
progress.finish(); progress.finish();
} }
} catch (IOException ex) {
return null;
} }
VideoCapture videoFile = new VideoCapture(); // will contain the video VideoCapture videoFile = new VideoCapture(); // will contain the video
if (!videoFile.open(tempFile.toString())) { if (!videoFile.open(tempFile.toString())) {
LOGGER.log(Level.WARNING, "Error opening {0} for preview generation.", ImageUtils.getContentPathSafe(file)); //NON-NLS
return null; return null;
} }
double fps = videoFile.get(CV_CAP_PROP_FPS); // gets frame per second double fps = videoFile.get(CV_CAP_PROP_FPS); // gets frame per second
double totalFrames = videoFile.get(CV_CAP_PROP_FRAME_COUNT); // gets total frames double totalFrames = videoFile.get(CV_CAP_PROP_FRAME_COUNT); // gets total frames
if (fps <= 0 || totalFrames <= 0) { if (fps <= 0 || totalFrames <= 0) {
LOGGER.log(Level.WARNING, "Error getting fps or total frames for {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS
return null; return null;
} }
double milliseconds = 1000 * (totalFrames / fps); //total milliseconds double milliseconds = 1000 * (totalFrames / fps); //total milliseconds
@ -132,10 +133,12 @@ public class VideoUtils {
for (int x = 0; x < THUMB_COLUMNS; x++) { for (int x = 0; x < THUMB_COLUMNS; x++) {
for (int y = 0; y < THUMB_ROWS; y++) { for (int y = 0; y < THUMB_ROWS; y++) {
if (!videoFile.set(CV_CAP_PROP_POS_MSEC, timestamp + x * framkeskip + y * framkeskip * THUMB_COLUMNS)) { 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 break; // if we can't set the time, return black for that frame
} }
//read the frame into the image/matrix //read the frame into the image/matrix
if (!videoFile.read(imageMatrix)) { 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 break; //if the image for some reason is bad, return black for that frame
} }

View File

@ -107,6 +107,8 @@ public enum FileTypeUtils {
, "sn", "ras" //sun raster NON-NLS , "sn", "ras" //sun raster NON-NLS
, "ico" //windows icons NON-NLS , "ico" //windows icons NON-NLS
, "tga" //targa 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 //add list of known video extensions
@ -129,6 +131,8 @@ public enum FileTypeUtils {
* mime types. * mime types.
*/ */
supportedMimeTypes.addAll(Arrays.asList("application/x-123")); //NON-NLS 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 //add list of mimetypes ImageIO claims to support
supportedMimeTypes.addAll(Stream.of(ImageIO.getReaderMIMETypes()) supportedMimeTypes.addAll(Stream.of(ImageIO.getReaderMIMETypes())

View File

@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.imagegallery;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -54,7 +55,6 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import org.apache.commons.lang3.StringUtils;
import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory; import org.netbeans.api.progress.ProgressHandleFactory;
import org.openide.util.Cancellable; import org.openide.util.Cancellable;
@ -85,6 +85,7 @@ import org.sleuthkit.datamodel.Image;
import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskData;
import org.sleuthkit.datamodel.VirtualDirectory;
/** /**
* Connects different parts of ImageGallery together and is hub for flow of * 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 SimpleBooleanProperty listeningEnabled = new SimpleBooleanProperty(false);
private final ReadOnlyIntegerWrapper queueSizeProperty = new ReadOnlyIntegerWrapper(0);
private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false); private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false);
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@ -153,7 +152,6 @@ public final class ImageGalleryController implements Executor {
private Node infoOverlay; private Node infoOverlay;
private SleuthkitCase sleuthKitCase; private SleuthkitCase sleuthKitCase;
// private NavPanel navPanel;
public ReadOnlyBooleanProperty getMetaDataCollapsed() { public ReadOnlyBooleanProperty getMetaDataCollapsed() {
return metaDataCollapsed.getReadOnlyProperty(); return metaDataCollapsed.getReadOnlyProperty();
@ -175,7 +173,7 @@ public final class ImageGalleryController implements Executor {
return historyManager.currentState(); return historyManager.currentState();
} }
public synchronized FileIDSelectionModel getSelectionModel() { public FileIDSelectionModel getSelectionModel() {
return selectionModel; return selectionModel;
} }
@ -187,13 +185,17 @@ public final class ImageGalleryController implements Executor {
return db; return db;
} }
synchronized public void setListeningEnabled(boolean enabled) { public void setListeningEnabled(boolean enabled) {
synchronized (listeningEnabled) {
listeningEnabled.set(enabled); listeningEnabled.set(enabled);
} }
}
synchronized boolean isListeningEnabled() { boolean isListeningEnabled() {
synchronized (listeningEnabled) {
return listeningEnabled.get(); return listeningEnabled.get();
} }
}
@ThreadConfined(type = ThreadConfined.ThreadType.ANY) @ThreadConfined(type = ThreadConfined.ThreadType.ANY)
void setStale(Boolean b) { void setStale(Boolean b) {
@ -248,12 +250,14 @@ public final class ImageGalleryController implements Executor {
checkForGroups(); checkForGroups();
}); });
IngestManager.getInstance().addIngestModuleEventListener((PropertyChangeEvent evt) -> { IngestManager ingestManager = IngestManager.getInstance();
Platform.runLater(this::updateRegroupDisabled); PropertyChangeListener ingestEventHandler =
}); propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled);
IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {
Platform.runLater(this::updateRegroupDisabled); ingestManager.addIngestModuleEventListener(ingestEventHandler);
}); ingestManager.addIngestJobEventListener(ingestEventHandler);
queueSizeProperty.addListener(obs -> this.updateRegroupDisabled());
} }
public ReadOnlyBooleanProperty getCanAdvance() { public ReadOnlyBooleanProperty getCanAdvance() {
@ -280,8 +284,9 @@ public final class ImageGalleryController implements Executor {
return historyManager.retreat(); return historyManager.retreat();
} }
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void updateRegroupDisabled() { 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. * aren't, add a blocking progress spinner with appropriate message.
*/ */
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@NbBundle.Messages({"ImageGalleryController.noGroupsDlg.msg1=No groups are fully analyzed; but listening to ingest is disabled. " + @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.", + " 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.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.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. " + "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.", + " 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.msg5=There are no images/videos in the added datasources.",
"ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:" + "ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:"
" the current Group By setting resulted in no groups, " + + " the current Group By setting resulted in no groups, "
"or no groups are fully analyzed but ingest is not running."}) + "or no groups are fully analyzed but ingest is not running."})
public void checkForGroups() { public void checkForGroups() {
if (groupManager.getAnalyzedGroups().isEmpty()) { if (groupManager.getAnalyzedGroups().isEmpty()) {
if (IngestManager.getInstance().isIngestRunning()) { if (IngestManager.getInstance().isIngestRunning()) {
@ -312,7 +317,7 @@ public final class ImageGalleryController implements Executor {
new ProgressIndicator())); new ProgressIndicator()));
} }
} else if (getFileUpdateQueueSizeProperty().get() > 0) { } else if (queueSizeProperty.get() > 0) {
replaceNotification(fullUIStackPane, replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(), new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(),
new ProgressIndicator())); new ProgressIndicator()));
@ -357,20 +362,14 @@ public final class ImageGalleryController implements Executor {
} }
} }
private void restartWorker() { synchronized private DBWorkerThread restartWorker() {
if (dbWorkerThread != null) { if (dbWorkerThread == null) {
dbWorkerThread = new DBWorkerThread(this);
dbWorkerThread.start();
} else {
// Keep using the same worker thread if one exists // Keep using the same worker thread if one exists
return;
} }
dbWorkerThread = new DBWorkerThread(); return 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();
} }
/** /**
@ -411,15 +410,16 @@ public final class ImageGalleryController implements Executor {
setListeningEnabled(false); setListeningEnabled(false);
ThumbnailCache.getDefault().clearCache(); ThumbnailCache.getDefault().clearCache();
historyManager.clear(); historyManager.clear();
groupManager.clear();
tagsManager.clearFollowUpTagName(); tagsManager.clearFollowUpTagName();
tagsManager.unregisterListener(groupManager); tagsManager.unregisterListener(groupManager);
tagsManager.unregisterListener(categoryManager); tagsManager.unregisterListener(categoryManager);
dbWorkerThread.cancelAllTasks(); dbWorkerThread.cancel();
dbWorkerThread = null; dbWorkerThread = null;
restartWorker(); dbWorkerThread = restartWorker();
Toolbar.getDefault(this).reset(); Toolbar.getDefault(this).reset();
groupManager.clear();
if (db != null) { if (db != null) {
db.closeDBCon(); db.closeDBCon();
} }
@ -431,11 +431,9 @@ public final class ImageGalleryController implements Executor {
* *
* @param innerTask * @param innerTask
*/ */
public void queueDBWorkerTask(InnerTask innerTask) { public synchronized void queueDBWorkerTask(BackgroundTask innerTask) {
// @@@ We could make a lock for the worker thread
if (dbWorkerThread == null) { if (dbWorkerThread == null) {
restartWorker(); dbWorkerThread = restartWorker();
} }
dbWorkerThread.addTask(innerTask); dbWorkerThread.addTask(innerTask);
} }
@ -455,10 +453,6 @@ public final class ImageGalleryController implements Executor {
Platform.runLater(this::checkForGroups); Platform.runLater(this::checkForGroups);
} }
public ReadOnlyIntegerProperty getFileUpdateQueueSizeProperty() {
return queueSizeProperty.getReadOnlyProperty();
}
public ReadOnlyDoubleProperty regroupProgress() { public ReadOnlyDoubleProperty regroupProgress() {
return groupManager.regroupProgress(); return groupManager.regroupProgress();
} }
@ -496,29 +490,43 @@ public final class ImageGalleryController implements Executor {
return undoManager; return undoManager;
} }
// @@@ REVIEW IF THIS SHOLD BE STATIC... public ReadOnlyIntegerProperty getDBTasksQueueSizeProperty() {
//TODO: concept seems like the controller deal with how much work to do at a given time 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.) // @@@ 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 // true if the process was requested to stop. Currently no way to reset it
private volatile boolean cancelled = false; private volatile boolean cancelled = false;
// list of tasks to run // 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. * Cancel all of the queued up tasks and the currently scheduled task.
* Note that after you cancel, you cannot submit new jobs to this * Note that after you cancel, you cannot submit new jobs to this
* thread. * thread.
*/ */
public void cancelAllTasks() { @Override
public boolean cancel() {
cancelled = true; cancelled = true;
for (InnerTask it : workQueue) { for (BackgroundTask it : workQueue) {
it.cancel(); it.cancel();
} }
workQueue.clear(); 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 * @param it
*/ */
public void addTask(InnerTask it) { public void addTask(BackgroundTask it) {
workQueue.add(it); workQueue.add(it);
Platform.runLater(() -> { int size = workQueue.size();
queueSizeProperty.set(workQueue.size()); Platform.runLater(() -> controller.queueSizeProperty.set(size));
});
} }
@Override @Override
@ -538,19 +545,17 @@ public final class ImageGalleryController implements Executor {
// nearly infinite loop waiting for tasks // nearly infinite loop waiting for tasks
while (true) { while (true) {
if (cancelled) { if (cancelled || isInterrupted()) {
return; return;
} }
try { try {
InnerTask it = workQueue.take(); BackgroundTask it = workQueue.take();
if (it.isCancelled() == false) { if (it.isCancelled() == false) {
it.run(); it.run();
} }
int size = workQueue.size();
Platform.runLater(() -> { Platform.runLater(() -> controller.queueSizeProperty.set(size));
queueSizeProperty.set(workQueue.size());
});
} catch (InterruptedException ex) { } catch (InterruptedException ex) {
LOGGER.log(Level.SEVERE, "Failed to run DB worker thread", ex); //NON-NLS LOGGER.log(Level.SEVERE, "Failed to run DB worker thread", ex); //NON-NLS
@ -568,7 +573,14 @@ public final class ImageGalleryController implements Executor {
*/ */
@NbBundle.Messages({"ImageGalleryController.InnerTask.progress.name=progress", @NbBundle.Messages({"ImageGalleryController.InnerTask.progress.name=progress",
"ImageGalleryController.InnerTask.message.name=status"}) "ImageGalleryController.InnerTask.message.name=status"})
static public abstract class InnerTask implements Runnable, Cancellable { 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() { public double getProgress() {
return progress.get(); return progress.get();
@ -585,9 +597,6 @@ public final class ImageGalleryController implements Executor {
public final void updateMessage(String Status) { public final void updateMessage(String Status) {
this.message.set(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() { public SimpleDoubleProperty progressProperty() {
return progress; return progress;
@ -601,24 +610,21 @@ public final class ImageGalleryController implements Executor {
return state.get(); return state.get();
} }
protected void updateState(Worker.State newState) {
state.set(newState);
}
public ReadOnlyObjectProperty<Worker.State> stateProperty() { public ReadOnlyObjectProperty<Worker.State> stateProperty() {
return new ReadOnlyObjectWrapper<>(state.get()); return new ReadOnlyObjectWrapper<>(state.get());
} }
protected InnerTask() {
}
@Override @Override
synchronized public boolean cancel() { public synchronized boolean cancel() {
updateState(Worker.State.CANCELLED); updateState(Worker.State.CANCELLED);
return true; return true;
} }
synchronized protected boolean isCancelled() { protected void updateState(Worker.State newState) {
state.set(newState);
}
protected synchronized boolean isCancelled() {
return getState() == Worker.State.CANCELLED; 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 * 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 AbstractFile file;
private final DrawableDB taskDB; private final DrawableDB taskDB;
@ -644,7 +650,6 @@ public final class ImageGalleryController implements Executor {
this.file = f; this.file = f;
this.taskDB = taskDB; this.taskDB = taskDB;
} }
} }
/** /**
@ -701,63 +706,63 @@ public final class ImageGalleryController implements Executor {
} }
} }
/** @NbBundle.Messages({"BulkTask.committingDb.status=commiting image/video database",
* Task that runs when image gallery listening is (re) enabled. "BulkTask.stopCopy.status=Stopping copy to drawable db task.",
* "BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."})
* Grabs all files with supported image/video mime types or extensions, and abstract static private class BulkTransferTask extends BackgroundTask {
* 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 {
private final ImageGalleryController controller; static private final String FILE_EXTENSION_CLAUSE =
private final DrawableDB taskDB; "(name LIKE '%." //NON-NLS
private final SleuthkitCase tskCase; + 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.controller = controller;
this.taskDB = taskDB; this.taskDB = taskDB;
this.tskCase = tskCase; this.tskCase = tskCase;
} }
static private final String FILE_EXTENSION_CLAUSE = abstract void cleanup(boolean success);
"(name LIKE '%." //NON-NLS
+ StringUtils.join(FileTypeUtils.getAllSupportedExtensions(), "' OR name LIKE '%.") //NON-NLS
+ "')";
static private final String MIMETYPE_CLAUSE = abstract List<AbstractFile> getFiles() throws TskCoreException;
"(mime_type LIKE '" //NON-NLS
+ StringUtils.join(FileTypeUtils.getAllSupportedMimeTypes(), "' OR mime_type LIKE '") //NON-NLS
+ "') ";
static private final String DRAWABLE_QUERY = abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) throws TskCoreException;
//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());
@Override @Override
public void run() { public void run() {
progressHandle = getInitialProgressHandle();
progressHandle.start(); progressHandle.start();
updateMessage(Bundle.CopyAnalyzedFiles_populatingDb_status()); updateMessage(Bundle.CopyAnalyzedFiles_populatingDb_status());
try { try {
//grab all files with supported extension or detected mime types //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()); progressHandle.switchToDeterminate(files.size());
updateProgress(0.0); updateProgress(0.0);
//do in transaction //do in transaction
DrawableDB.DrawableTransaction tr = taskDB.beginTransaction(); DrawableDB.DrawableTransaction tr = taskDB.beginTransaction();
int units = 0; int workDone = 0;
for (final AbstractFile f : files) { for (final AbstractFile f : files) {
if (isCancelled()) { if (isCancelled()) {
LOGGER.log(Level.WARNING, "Task cancelled: not all contents may be transfered to drawable database."); //NON-NLS LOGGER.log(Level.WARNING, "Task cancelled: not all contents may be transfered to drawable database."); //NON-NLS
@ -765,6 +770,67 @@ public final class ImageGalleryController implements Executor {
break; break;
} }
processFile(f, tr);
workDone++;
progressHandle.progress(f.getName(), workDone);
updateProgress(workDone - 1 / (double) files.size());
updateMessage(f.getName());
}
progressHandle.finish();
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.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);
}
cleanup(true);
}
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; final boolean known = f.getKnown() == TskData.FileKnown.KNOWN;
if (known) { if (known) {
@ -789,137 +855,87 @@ public final class ImageGalleryController implements Executor {
} }
} }
} }
units++;
final int prog = units;
progressHandle.progress(f.getName(), units);
updateProgress(prog - 1 / (double) files.size());
updateMessage(f.getName());
} }
progressHandle.finish(); @Override
@NbBundle.Messages({"CopyAnalyzedFiles.populatingDb.status=populating analyzed image/video database",})
progressHandle = ProgressHandleFactory.createHandle(Bundle.CopyAnalyzedFiles_committingDb_status()); ProgressHandle getInitialProgressHandle() {
updateMessage(Bundle.CopyAnalyzedFiles_committingDb_status()); return ProgressHandleFactory.createHandle(Bundle.CopyAnalyzedFiles_populatingDb_status(), this);
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.finish();
updateMessage("");
updateProgress(-1.0);
controller.setStale(true);
return;
}
progressHandle.finish();
updateMessage("");
updateProgress(-1.0);
controller.setStale(false);
} }
} }
/** /**
* task that does pre-ingest copy over of files from a new datasource (uses * Copy files from a newly added data source into the DB. Get all "drawable"
* fs_obj_id to identify files from new datasources) * 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 * TODO: create methods to simplify progress value/text updates to both
* netbeans and ImageGallery progress/status * netbeans and ImageGallery progress/status
*/ */
@NbBundle.Messages({"PrePopulateDataSourceFiles.prepopulatingDb.status=prepopulating image/video database", @NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=commiting image/video database"})
"PrePopulateDataSourceFiles.committingDb.status=commiting image/video database"}) static private class PrePopulateDataSourceFiles extends BulkTransferTask {
private class PrePopulateDataSourceFiles extends InnerTask {
private static final Logger LOGGER = Logger.getLogger(PrePopulateDataSourceFiles.class.getName());
private final Content dataSource; 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 * @param dataSourceId Data source object ID
*/ */
PrePopulateDataSourceFiles(Content dataSource) { PrePopulateDataSourceFiles(Content dataSource, ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
super(); super(controller, taskDB, tskCase);
this.dataSource = dataSource; 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 @Override
public void run() { protected void cleanup(boolean success) {
progressHandle.start(); }
updateMessage(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status());
try { @Override
String fsQuery = "(fs_obj_id IS NULL) "; //default clause NON-NLS void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) {
/* taskDB.insertFile(DrawableFile.create(f, false, false), tr);
* 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 @Override
* the drawable DB is smart enough to de-dupe them. For Images List<AbstractFile> getFiles() throws TskCoreException {
* we can do better.
*/
if (dataSource instanceof Image) { if (dataSource instanceof Image) {
List<FileSystem> fileSystems = ((Image) dataSource).getFileSystems(); List<FileSystem> fileSystems = ((Image) dataSource).getFileSystems();
if (fileSystems.isEmpty()) { if (fileSystems.isEmpty()) {
/* /*
* no filesystems, don't bother with the initial * no filesystems, don't bother with the initial population,
* population, just sort things out on file_done events * just sort things out on file_done events
*/ */
progressHandle.finish(); progressHandle.finish();
return; return Collections.emptyList();
} }
//use this clause to only grab files from the newly added filesystems. //use this clause to only grab files from the newly added filesystems.
fsQuery = fileSystems.stream() String fsQuery = fileSystems.stream()
.map(fileSystem -> String.valueOf(fileSystem.getId())) .map(fileSystem -> String.valueOf(fileSystem.getId()))
.collect(Collectors.joining(" OR fs_obj_id = ", "(fs_obj_id = ", ") ")); //NON-NLS .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) {
/*
* 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.
*/
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);
}
} }
final List<AbstractFile> files = getSleuthKitCase().findAllFilesWhere(fsQuery + " AND " + DRAWABLE_QUERY); //NON-NLS @Override
progressHandle.switchToDeterminate(files.size()); @NbBundle.Messages({"PrePopulateDataSourceFiles.prepopulatingDb.status=prepopulating image/video database",})
ProgressHandle getInitialProgressHandle() {
//do in transaction return ProgressHandleFactory.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this);
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
}
progressHandle.finish();
} }
} }
@ -1011,7 +1027,7 @@ public final class ImageGalleryController implements Executor {
//copy all file data to drawable databse //copy all file data to drawable databse
Content newDataSource = (Content) evt.getNewValue(); Content newDataSource = (Content) evt.getNewValue();
if (isListeningEnabled()) { if (isListeningEnabled()) {
queueDBWorkerTask(new PrePopulateDataSourceFiles(newDataSource)); queueDBWorkerTask(new PrePopulateDataSourceFiles(newDataSource, ImageGalleryController.this, getDatabase(), getSleuthKitCase()));
} else {//TODO: keep track of what we missed for later } else {//TODO: keep track of what we missed for later
setStale(true); setStale(true);
} }
@ -1028,7 +1044,6 @@ public final class ImageGalleryController implements Executor {
getTagsManager().fireTagDeletedEvent(tagDeletedEvent); getTagsManager().fireTagDeletedEvent(tagDeletedEvent);
} }
break; break;
} }
} }
} }

View File

@ -157,14 +157,15 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
hashHitList = new HashHitGroupList(controller); hashHitList = new HashHitGroupList(controller);
TabPane tabPane = new TabPane(groupTree, hashHitList); TabPane tabPane = new TabPane(groupTree, hashHitList);
tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE);
tabPane.setMinWidth(TabPane.USE_PREF_SIZE);
VBox.setVgrow(tabPane, Priority.ALWAYS); VBox.setVgrow(tabPane, Priority.ALWAYS);
leftPane = new VBox(tabPane, new SummaryTablePane(controller)); leftPane = new VBox(tabPane, new SummaryTablePane(controller));
SplitPane.setResizableWithParent(leftPane, Boolean.FALSE); SplitPane.setResizableWithParent(leftPane, Boolean.FALSE);
SplitPane.setResizableWithParent(groupPane, Boolean.TRUE); SplitPane.setResizableWithParent(groupPane, Boolean.TRUE);
SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE); SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE);
splitPane.getItems().addAll(leftPane, centralStack, metaDataTable); 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().setStacks(fullUIStack, centralStack);
ImageGalleryController.getDefault().setShowTree(() -> tabPane.getSelectionModel().select(groupTree)); ImageGalleryController.getDefault().setShowTree(() -> tabPane.getSelectionModel().select(groupTree));

View File

@ -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();
}
}

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013 Basis Technology Corp. * Copyright 2013-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org * Contact: carrier <at> sleuthkit <dot> org
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * 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.awt.Window;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set; 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.Menu;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.image.ImageView;
import javax.swing.SwingUtilities; 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.util.NbBundle;
import org.openide.windows.TopComponent; import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager; import org.openide.windows.WindowManager;
import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog; import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog;
import org.sleuthkit.autopsy.actions.GetTagNameDialog; import org.sleuthkit.autopsy.actions.GetTagNameDialog;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; 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.TagName;
import org.sleuthkit.datamodel.TskCoreException;
/** /**
* An abstract base class for actions that allow users to tag SleuthKit data * Instances of this Action allow users to apply tags to content.
* 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.
*/ */
abstract class AddTagAction { public class AddTagAction extends Action {
protected static final String NO_COMMENT = ""; private static final Logger LOGGER = Logger.getLogger(AddTagAction.class.getName());
/** private final ImageGalleryController controller;
* Template method to allow derived classes to provide a string for a menu private final Set<Long> selectedFileIDs;
* item label. private final TagName tagName;
*/
abstract protected String getActionDisplayName();
/** public AddTagAction(ImageGalleryController controller, TagName tagName, Set<Long> selectedFileIDs) {
* Template method to allow derived classes to add the indicated tag and super(tagName.getDisplayName());
* comment to one or more a SleuthKit data model objects. this.controller = controller;
*/ this.selectedFileIDs = selectedFileIDs;
abstract protected void addTag(TagName tagName, String comment); this.tagName = tagName;
setGraphic(controller.getTagsManager().getGraphic(tagName));
setText(tagName.getDisplayName());
setEventHandler(actionEvent -> addTagWithComment(""));
}
/** static public Menu getTagMenu(ImageGalleryController controller) {
* Template method to allow derived classes to add the indicated tag and return new TagMenu(controller);
* comment to a list of one or more file IDs. }
*/
abstract protected void addTagsToFiles(TagName tagName, String comment, Set<Long> selectedFiles); 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", @NbBundle.Messages({"AddTagAction.menuItem.quickTag=Quick Tag",
"AddTagAction.menuItem.noTags=No tags", "AddTagAction.menuItem.noTags=No tags",
"AddTagAction.menuItem.newTag=New Tag...", "AddTagAction.menuItem.newTag=New Tag...",
"AddTagAction.menuItem.tagAndComment=Tag and Comment..."}) "AddTagAction.menuItem.tagAndComment=Tag and Comment...",
protected class TagMenu extends Menu { "AddDrawableTagAction.displayName.plural=Tag Files",
"AddDrawableTagAction.displayName.singular=Tag File"})
private static class TagMenu extends Menu {
TagMenu(ImageGalleryController controller) { 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. // Create a "Quick Tag" sub-menu.
Menu quickTagMenu = new Menu(Bundle.AddTagAction_menuItem_quickTag()); Menu quickTagMenu = new Menu(Bundle.AddTagAction_menuItem_quickTag());
@ -98,10 +159,8 @@ abstract class AddTagAction {
quickTagMenu.getItems().add(empty); quickTagMenu.getItems().add(empty);
} else { } else {
for (final TagName tagName : tagNames) { for (final TagName tagName : tagNames) {
MenuItem tagNameItem = new MenuItem(tagName.getDisplayName()); AddTagAction addDrawableTagAction = new AddTagAction(controller, tagName, selectedFileIDs);
tagNameItem.setOnAction((ActionEvent t) -> { MenuItem tagNameItem = ActionUtils.createMenuItem(addDrawableTagAction);
addTag(tagName, NO_COMMENT);
});
quickTagMenu.getItems().add(tagNameItem); quickTagMenu.getItems().add(tagNameItem);
} }
} }
@ -112,14 +171,13 @@ abstract class AddTagAction {
* or select a tag name and adds a tag with the resulting name. * or select a tag name and adds a tag with the resulting name.
*/ */
MenuItem newTagMenuItem = new MenuItem(Bundle.AddTagAction_menuItem_newTag()); MenuItem newTagMenuItem = new MenuItem(Bundle.AddTagAction_menuItem_newTag());
newTagMenuItem.setOnAction((ActionEvent t) -> { newTagMenuItem.setOnAction(actionEvent ->
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
TagName tagName = GetTagNameDialog.doDialog(getIGWindow()); TagName tagName = GetTagNameDialog.doDialog(getIGWindow());
if (tagName != null) { if (tagName != null) {
addTag(tagName, NO_COMMENT); new AddTagAction(controller, tagName, selectedFileIDs).handle(actionEvent);
} }
}); }));
});
quickTagMenu.getItems().add(newTagMenuItem); quickTagMenu.getItems().add(newTagMenuItem);
/* /*
@ -129,26 +187,17 @@ abstract class AddTagAction {
* name. * name.
*/ */
MenuItem tagAndCommentItem = new MenuItem(Bundle.AddTagAction_menuItem_tagAndComment()); MenuItem tagAndCommentItem = new MenuItem(Bundle.AddTagAction_menuItem_tagAndComment());
tagAndCommentItem.setOnAction((ActionEvent t) -> { tagAndCommentItem.setOnAction(actionEvent ->
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
GetTagNameAndCommentDialog.TagNameAndComment tagNameAndComment = GetTagNameAndCommentDialog.doDialog(getIGWindow()); GetTagNameAndCommentDialog.TagNameAndComment tagNameAndComment = GetTagNameAndCommentDialog.doDialog(getIGWindow());
if (null != tagNameAndComment) { if (null != tagNameAndComment) {
if (CategoryManager.isCategoryTagName(tagNameAndComment.getTagName())) { new AddTagAction(controller, tagNameAndComment.getTagName(), selectedFileIDs).addTagWithComment(tagNameAndComment.getComment());
new CategorizeAction(controller).addTag(tagNameAndComment.getTagName(), tagNameAndComment.getComment());
} else {
new AddDrawableTagAction(controller).addTag(tagNameAndComment.getTagName(), tagNameAndComment.getComment());
} }
} }));
});
});
getItems().add(tagAndCommentItem); getItems().add(tagAndCommentItem);
} }
} }
/**
* @return the Window containing the ImageGalleryTopComponent
*/
static private Window getIGWindow() { static private Window getIGWindow() {
TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID); TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID);
return SwingUtilities.getWindowAncestor(etc); return SwingUtilities.getWindowAncestor(etc);

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013 Basis Technology Corp. * Copyright 2013-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org * Contact: carrier <at> sleuthkit <dot> org
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * 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 com.google.common.collect.ImmutableMap;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.event.ActionEvent; import javafx.collections.ObservableSet;
import javafx.scene.control.Menu; import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCodeCombination;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; 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.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.ContentTag;
@ -49,48 +51,42 @@ import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException; 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"}) @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 static final Logger LOGGER = Logger.getLogger(CategorizeAction.class.getName());
private final ImageGalleryController controller; private final ImageGalleryController controller;
private final UndoRedoManager undoManager; private final UndoRedoManager undoManager;
private final Category cat;
private final Set<Long> selectedFileIDs;
private final Boolean createUndo;
public CategorizeAction(ImageGalleryController controller) { public CategorizeAction(ImageGalleryController controller, Category cat, Set<Long> selectedFileIDs) {
super(); this(controller, cat, selectedFileIDs, true);
this.controller = controller;
undoManager = controller.getUndoManager();
} }
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); return new CategoryMenu(controller);
} }
@Override private void addCatToFiles() {
protected String getActionDisplayName() { Logger.getAnonymousLogger().log(Level.INFO, "categorizing{0} as {1}", new Object[]{selectedFileIDs.toString(), cat.getDisplayName()}); //NON-NLS
return Bundle.CategorizeAction_displayName(); controller.queueDBWorkerTask(new CategorizeTask(selectedFileIDs, cat, createUndo));
}
@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));
} }
/** /**
@ -101,17 +97,13 @@ public class CategorizeAction extends AddTagAction {
CategoryMenu(ImageGalleryController controller) { CategoryMenu(ImageGalleryController controller) {
super(Bundle.CategorizeAction_displayName()); 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 // Each category get an item in the sub-menu. Selecting one of these menu items adds
// a tag with the associated category. // a tag with the associated category.
for (final Category cat : Category.values()) { for (final Category cat : Category.values()) {
MenuItem categoryItem = ActionUtils.createMenuItem(new CategorizeAction(controller, cat, selected));
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()))));
getItems().add(categoryItem); getItems().add(categoryItem);
} }
} }
@ -120,30 +112,28 @@ public class CategorizeAction extends AddTagAction {
@NbBundle.Messages({"# {0} - fileID number", @NbBundle.Messages({"# {0} - fileID number",
"CategorizeTask.errorUnable.msg=Unable to categorize {0}.", "CategorizeTask.errorUnable.msg=Unable to categorize {0}.",
"CategorizeTask.errorUnable.title=Categorizing Error"}) "CategorizeTask.errorUnable.title=Categorizing Error"})
private class CategorizeTask extends ImageGalleryController.InnerTask { private class CategorizeTask extends ImageGalleryController.BackgroundTask {
private final Set<Long> fileIDs; 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(); super();
this.fileIDs = fileIDs; this.fileIDs = fileIDs;
java.util.Objects.requireNonNull(tagName); java.util.Objects.requireNonNull(cat);
this.tagName = tagName; this.cat = cat;
this.comment = comment;
this.createUndo = createUndo; this.createUndo = createUndo;
} }
@Override @Override
public void run() { public void run() {
final DrawableTagsManager tagsManager = controller.getTagsManager(); final DrawableTagsManager tagsManager = controller.getTagsManager();
final CategoryManager categoryManager = controller.getCategoryManager(); 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) { for (long fileID : fileIDs) {
try { try {
DrawableFile<?> file = controller.getFileFromId(fileID); //drawable db access DrawableFile<?> file = controller.getFileFromId(fileID); //drawable db access
@ -151,12 +141,12 @@ public class CategorizeAction extends AddTagAction {
Category oldCat = file.getCategory(); //drawable db access Category oldCat = file.getCategory(); //drawable db access
TagName oldCatTagName = categoryManager.getTagName(oldCat); TagName oldCatTagName = categoryManager.getTagName(oldCat);
if (false == tagName.equals(oldCatTagName)) { if (false == tagName.equals(oldCatTagName)) {
oldCats.put(fileID, oldCatTagName); oldCats.put(fileID, oldCat);
} }
} }
final List<ContentTag> fileTags = tagsManager.getContentTagsByContent(file); final List<ContentTag> fileTags = tagsManager.getContentTagsByContent(file);
if (tagName == categoryManager.getTagName(Category.ZERO)) { if (tagName.equals(catZeroTagName)) {
// delete all cat tags for cat-0 // delete all cat tags for cat-0
fileTags.stream() fileTags.stream()
.filter(tag -> CategoryManager.isCategoryTagName(tag.getName())) .filter(tag -> CategoryManager.isCategoryTagName(tag.getName()))
@ -173,7 +163,7 @@ public class CategorizeAction extends AddTagAction {
.map(Tag::getName) .map(Tag::getName)
.filter(tagName::equals) .filter(tagName::equals)
.collect(Collectors.toList()).isEmpty()) { .collect(Collectors.toList()).isEmpty()) {
tagsManager.addContentTag(file, tagName, comment); tagsManager.addContentTag(file, tagName, "");
} }
} }
} catch (TskCoreException ex) { } catch (TskCoreException ex) {
@ -186,7 +176,7 @@ public class CategorizeAction extends AddTagAction {
} }
if (createUndo && oldCats.isEmpty() == false) { 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 @Immutable
private final class CategorizationChange implements UndoRedoManager.UndoableCommand { private final class CategorizationChange implements UndoRedoManager.UndoableCommand {
private final TagName newCategory; private final Category newCategory;
private final ImmutableMap<Long, TagName> oldCategories; private final ImmutableMap<Long, Category> oldCategories;
private final ImageGalleryController controller; 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.controller = controller;
this.newCategory = newCategory; this.newCategory = newCategory;
this.oldCategories = ImmutableMap.copyOf(oldCategories); this.oldCategories = ImmutableMap.copyOf(oldCategories);
@ -213,8 +203,8 @@ public class CategorizeAction extends AddTagAction {
*/ */
@Override @Override
public void run() { public void run() {
CategorizeAction categorizeAction = new CategorizeAction(controller); CategorizeAction categorizeAction = new CategorizeAction(controller, newCategory, this.oldCategories.keySet(), false);
categorizeAction.addTagsToFiles(newCategory, "", this.oldCategories.keySet(), false); categorizeAction.addCatToFiles();
} }
/** /**
@ -223,9 +213,10 @@ public class CategorizeAction extends AddTagAction {
*/ */
@Override @Override
public void undo() { public void undo() {
CategorizeAction categorizeAction = new CategorizeAction(controller);
for (Map.Entry<Long, TagName> entry : oldCategories.entrySet()) { for (Map.Entry<Long, Category> entry : oldCategories.entrySet()) {
categorizeAction.addTagsToFiles(entry.getValue(), "", Collections.singleton(entry.getKey()), false); new CategorizeAction(controller, entry.getValue(), Collections.singleton(entry.getKey()), false)
.addCatToFiles();
} }
} }
} }

View File

@ -19,21 +19,19 @@
package org.sleuthkit.autopsy.imagegallery.actions; package org.sleuthkit.autopsy.imagegallery.actions;
import com.google.common.collect.ImmutableSet; 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.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
/** /**
* *
*/ */
public class CategorizeGroupAction extends Action { public class CategorizeGroupAction extends CategorizeAction {
public CategorizeGroupAction(Category cat, ImageGalleryController controller) { public CategorizeGroupAction(Category cat, ImageGalleryController controller) {
super(cat.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> { super(controller, cat, null);
Set<Long> fileIdSet = ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs()); setEventHandler(actionEvent ->
new CategorizeAction(controller).addTagsToFiles(controller.getTagsManager().getTagName(cat), "", fileIdSet); new CategorizeAction(controller, cat, ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs()))
}); .handle(actionEvent)
setGraphic(cat.getGraphic()); );
} }
} }

View File

@ -18,17 +18,19 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.actions; package org.sleuthkit.autopsy.imagegallery.actions;
import org.controlsfx.control.action.Action;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
/** /**
* *
*/ */
public class CategorizeSelectedFilesAction extends Action { public class CategorizeSelectedFilesAction extends CategorizeAction {
public CategorizeSelectedFilesAction(Category cat, ImageGalleryController controller) { public CategorizeSelectedFilesAction(Category cat, ImageGalleryController controller) {
super(cat.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(cat), "")); super(controller, cat, null);
setGraphic(cat.getGraphic()); setEventHandler(actionEvent ->
new CategorizeAction(controller, cat, controller.getSelectionModel().getSelected())
.handle(actionEvent)
);
} }
} }

View File

@ -19,21 +19,19 @@
package org.sleuthkit.autopsy.imagegallery.actions; package org.sleuthkit.autopsy.imagegallery.actions;
import com.google.common.collect.ImmutableSet; 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.ImageGalleryController;
import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TagName;
/** /**
* *
*/ */
public class TagGroupAction extends Action { public class TagGroupAction extends AddTagAction {
public TagGroupAction(final TagName tagName, ImageGalleryController controller) { public TagGroupAction(final TagName tagName, ImageGalleryController controller) {
super(tagName.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> { super(controller, tagName, null);
Set<Long> fileIdSet = ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs()); setEventHandler(actionEvent ->
new AddDrawableTagAction(controller).addTagsToFiles(tagName, "", fileIdSet); new AddTagAction(controller, tagName, ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs())).
}); handle(actionEvent)
setGraphic(controller.getTagsManager().getGraphic(tagName)); );
} }
} }

View File

@ -18,17 +18,19 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.actions; package org.sleuthkit.autopsy.imagegallery.actions;
import org.controlsfx.control.action.Action;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TagName;
/** /**
* *
*/ */
public class TagSelectedFilesAction extends Action { public class TagSelectedFilesAction extends AddTagAction {
public TagSelectedFilesAction(final TagName tagName, ImageGalleryController controller) { public TagSelectedFilesAction(final TagName tagName, ImageGalleryController controller) {
super(tagName.getDisplayName(), actionEvent -> new AddDrawableTagAction(controller).addTag(tagName, "")); super(controller, tagName, null);
setGraphic(controller.getTagsManager().getGraphic(tagName)); setEventHandler(actionEvent ->
new AddTagAction(controller, tagName, controller.getSelectionModel().getSelected()).
handle(actionEvent)
);
} }
} }

View File

@ -25,6 +25,9 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Node; 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.Background;
import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border; import javafx.scene.layout.Border;
@ -94,6 +97,7 @@ public enum Category {
private final String displayName; private final String displayName;
private final int id; private final int id;
private Image snapshot;
private Category(Color color, int id, String name) { private Category(Color color, int id, String name) {
this.color = color; this.color = color;
@ -118,11 +122,15 @@ public enum Category {
return displayName; return displayName;
} }
public Node getGraphic() { synchronized public Node getGraphic() {
if (snapshot == null) {
Region region = new Region(); Region region = new Region();
region.setBackground(new Background(new BackgroundFill(getColor(), CORNER_RADII_4, Insets.EMPTY))); region.setBackground(new Background(new BackgroundFill(getColor(), CORNER_RADII_4, Insets.EMPTY)));
region.setPrefSize(16, 16); region.setPrefSize(16, 16);
region.setBorder(new Border(new BorderStroke(getColor().darker(), BorderStrokeStyle.SOLID, CORNER_RADII_4, BORDER_WIDTHS_2))); region.setBorder(new Border(new BorderStroke(getColor().darker(), BorderStrokeStyle.SOLID, CORNER_RADII_4, BORDER_WIDTHS_2)));
return region; Scene scene = new Scene(region, 16, 16, Color.TRANSPARENT);
snapshot = region.snapshot(null, null);
}
return new ImageView(snapshot);
} }
} }

View File

@ -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 StringBuilder query = new StringBuilder("SELECT " + groupBy.attrName.toString() + ", COUNT(*) FROM drawable_files GROUP BY " + groupBy.attrName.toString()); //NON-NLS
String orderByClause = ""; String orderByClause = "";
switch (sortBy) {
case GROUP_BY_VALUE: if (sortBy == GROUP_BY_VALUE) {
orderByClause = " ORDER BY " + groupBy.attrName.toString(); //NON-NLS orderByClause = " ORDER BY " + groupBy.attrName.toString();
break; } else if (sortBy == GroupSortBy.FILE_COUNT) {
case FILE_COUNT: orderByClause = " ORDER BY COUNT(*)";
orderByClause = " ORDER BY COUNT(*)"; //NON-NLS
break;
case NONE:
// case PRIORITY:
break;
} }
query.append(orderByClause); query.append(orderByClause);

View File

@ -45,13 +45,24 @@ import org.sleuthkit.datamodel.TskCoreException;
* Manages Tags, Tagging, and the relationship between Categories and Tags in * Manages Tags, Tagging, and the relationship between Categories and Tags in
* the autopsy Db. Delegates some work to the backing {@link TagsManager}. * the autopsy Db. Delegates some work to the backing {@link TagsManager}.
*/ */
@NbBundle.Messages({"DrawableTagsManager.followUp=Follow Up"}) @NbBundle.Messages({"DrawableTagsManager.followUp=Follow Up",
"DrawableTagsManager.bookMark=Bookmark"})
public class DrawableTagsManager { public class DrawableTagsManager {
private static final Logger LOGGER = Logger.getLogger(DrawableTagsManager.class.getName()); private static final Logger LOGGER = Logger.getLogger(DrawableTagsManager.class.getName());
private static final String FOLLOW_UP = Bundle.DrawableTagsManager_followUp(); 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 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(); final private Object autopsyTagsManagerLock = new Object();
private TagsManager autopsyTagsManager; private TagsManager autopsyTagsManager;
@ -70,6 +81,7 @@ public class DrawableTagsManager {
* The tag name corresponding to the "built-in" tag "Follow Up" * The tag name corresponding to the "built-in" tag "Follow Up"
*/ */
private TagName followUpTagName; private TagName followUpTagName;
private TagName bookmarkTagName;
public DrawableTagsManager(TagsManager autopsyTagsManager) { public DrawableTagsManager(TagsManager autopsyTagsManager) {
this.autopsyTagsManager = 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 * get all the TagNames that are not categories
* *
@ -192,7 +213,7 @@ public class DrawableTagsManager {
} catch (TagsManager.TagNameAlreadyExistsException ex) { } catch (TagsManager.TagNameAlreadyExistsException ex) {
throw new TskCoreException("tagame exists but wasn't found", 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 LOGGER.log(Level.SEVERE, "Case was closed out from underneath", ex); //NON-NLS
throw new TskCoreException("Case was closed out from underneath", ex); throw new TskCoreException("Case was closed out from underneath", ex);
} }
@ -241,9 +262,11 @@ public class DrawableTagsManager {
try { try {
if (tagname.equals(getFollowUpTagName())) { if (tagname.equals(getFollowUpTagName())) {
return new ImageView(getFollowUpImage()); return new ImageView(getFollowUpImage());
} else if (tagname.equals(getBookmarkTagName())) {
return new ImageView(getBookmarkImage());
} }
} catch (TskCoreException ex) { } 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); return DrawableAttribute.TAGS.getGraphicForValue(tagname);
} }
@ -254,4 +277,12 @@ public class DrawableTagsManager {
} }
return FOLLOW_UP_IMAGE; return FOLLOW_UP_IMAGE;
} }
synchronized private static Image getBookmarkImage() {
if (BOOKMARK_IMAGE == null) {
BOOKMARK_IMAGE = new Image("/org/sleuthkit/autopsy/images/star-bookmark-icon-16.png");
}
return BOOKMARK_IMAGE;
}
} }

View File

@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -126,7 +127,7 @@ public class GroupManager {
private volatile DrawableAttribute<?> groupBy = DrawableAttribute.PATH; private volatile DrawableAttribute<?> groupBy = DrawableAttribute.PATH;
private volatile SortOrder sortOrder = SortOrder.ASCENDING; 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< DrawableAttribute<?>> groupByProp = new ReadOnlyObjectWrapper<>(groupBy);
private final ReadOnlyObjectWrapper<SortOrder> sortOrderProp = new ReadOnlyObjectWrapper<>(sortOrder); private final ReadOnlyObjectWrapper<SortOrder> sortOrderProp = new ReadOnlyObjectWrapper<>(sortOrder);
@ -274,7 +275,7 @@ public class GroupManager {
} else if (unSeenGroups.contains(group) == false) { } else if (unSeenGroups.contains(group) == false) {
unSeenGroups.add(group); unSeenGroups.add(group);
} }
FXCollections.sort(unSeenGroups, sortBy.getGrpComparator(sortOrder)); FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
} }
/** /**
@ -299,11 +300,11 @@ public class GroupManager {
Platform.runLater(() -> { Platform.runLater(() -> {
if (analyzedGroups.contains(group)) { if (analyzedGroups.contains(group)) {
analyzedGroups.remove(group); analyzedGroups.remove(group);
FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder)); FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
} }
if (unSeenGroups.contains(group)) { if (unSeenGroups.contains(group)) {
unSeenGroups.remove(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; return sortBy;
} }
@ -459,7 +460,7 @@ public class GroupManager {
Platform.runLater(() -> sortByProp.set(sortBy)); Platform.runLater(() -> sortByProp.set(sortBy));
} }
public ReadOnlyObjectProperty<GroupSortBy> getSortByProperty() { public ReadOnlyObjectProperty< Comparator<DrawableGroup>> getSortByProperty() {
return sortByProp.getReadOnlyProperty(); return sortByProp.getReadOnlyProperty();
} }
@ -523,8 +524,8 @@ public class GroupManager {
setSortBy(sortBy); setSortBy(sortBy);
setSortOrder(sortOrder); setSortOrder(sortOrder);
Platform.runLater(() -> { Platform.runLater(() -> {
FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder)); FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
FXCollections.sort(unSeenGroups, sortBy.getGrpComparator(sortOrder)); FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
}); });
} }
} }
@ -666,7 +667,7 @@ public class GroupManager {
group = new DrawableGroup(groupKey, fileIDs, groupSeen); group = new DrawableGroup(groupKey, fileIDs, groupSeen);
controller.getCategoryManager().registerListener(group); controller.getCategoryManager().registerListener(group);
group.seenProperty().addListener((o, oldSeen, newSeen) -> { group.seenProperty().addListener((o, oldSeen, newSeen) -> {
markGroupSeen(group, newSeen); Platform.runLater(() -> markGroupSeen(group, newSeen));
}); });
groupMap.put(groupKey, group); groupMap.put(groupKey, group);
} }
@ -675,7 +676,7 @@ public class GroupManager {
if (analyzedGroups.contains(group) == false) { if (analyzedGroups.contains(group) == false) {
analyzedGroups.add(group); analyzedGroups.add(group);
if (Objects.isNull(task)) { if (Objects.isNull(task)) {
FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder)); FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
} }
} }
markGroupSeen(group, groupSeen); markGroupSeen(group, groupSeen);
@ -735,8 +736,8 @@ public class GroupManager {
private final SortOrder sortOrder; private final SortOrder sortOrder;
public ReGroupTask(DrawableAttribute<A> groupBy, GroupSortBy sortBy, SortOrder sortOrder) { ReGroupTask(DrawableAttribute<A> groupBy, GroupSortBy sortBy, SortOrder sortOrder) {
super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.name(), sortOrder.toString()), true); super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), true);
this.groupBy = groupBy; this.groupBy = groupBy;
this.sortBy = sortBy; this.sortBy = sortBy;
@ -755,7 +756,7 @@ public class GroupManager {
return null; 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(() -> { Platform.runLater(() -> {
analyzedGroups.clear(); analyzedGroups.clear();
unSeenGroups.clear(); unSeenGroups.clear();
@ -778,7 +779,7 @@ public class GroupManager {
groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p); groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p);
popuplateIfAnalyzed(new GroupKey<A>(groupBy, val), this); 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); updateProgress(1, 1);
return null; 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<>();
}
}
} }

View File

@ -18,89 +18,50 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.datamodel.grouping; package org.sleuthkit.autopsy.imagegallery.datamodel.grouping;
import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.scene.image.Image; 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.apache.commons.lang3.StringUtils;
import org.openide.util.NbBundle; 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 * Pseudo enum of possible properties to sort groups by.
* down in Toolbar as well as each enum value having the stategy
* ({@link Comparator}) for sorting the groups
*/ */
@NbBundle.Messages({"GroupSortBy.groupSize=Group Size", @NbBundle.Messages({"GroupSortBy.groupSize=Group Size",
"GroupSortBy.groupName=Group Name", "GroupSortBy.groupName=Group Name",
"GroupSortBy.none=None", "GroupSortBy.none=None",
"GroupSortBy.priority=Priority"}) "GroupSortBy.priority=Priority"})
public enum GroupSortBy implements ComparatorProvider { public class GroupSortBy implements Comparator<DrawableGroup> {
/** /**
* sort the groups by the number of files in each sort the groups by the * sort the groups by the number of files in each
* number of files in each
*/ */
FILE_COUNT(Bundle.GroupSortBy_groupSize(), true, "folder-open-image.png") { //NON-NLS public final static GroupSortBy FILE_COUNT = new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png", Comparator.comparing(DrawableGroup::getSize));
@Override
public Comparator<DrawableGroup> getGrpComparator(final SortOrder sortOrder) {
return applySortOrder(sortOrder, Comparator.comparingInt(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 * sort the groups by the natural order of the grouping value ( eg group
* them by path alphabetically ) * them by path alphabetically )
*/ */
GROUP_BY_VALUE(Bundle.GroupSortBy_groupName(), true, "folder-rename.png") { //NON-NLS public final static GroupSortBy GROUP_BY_VALUE = new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png", Comparator.comparing(DrawableGroup::getGroupByValueDislpayName));
@Override
public Comparator<DrawableGroup> getGrpComparator(final SortOrder sortOrder) {
return applySortOrder(sortOrder, Comparator.comparing(t -> t.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 * don't sort the groups just use what ever order they come in (ingest
* order) * order)
*/ */
NONE(Bundle.GroupSortBy_none(), false, "prohibition.png") { //NON-NLS public final static GroupSortBy NONE = new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png", new AllEqualComparator<>());
@Override
public Comparator<DrawableGroup> getGrpComparator(SortOrder sortOrder) {
return new NoOpComparator<>();
}
@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 * sort the groups by some priority metric to be determined and implemented
*/ */
PRIORITY(Bundle.GroupSortBy_priority(), false, "hashset_hits.png") { //NON-NLS public final static GroupSortBy PRIORITY = new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png", Comparator.comparing(DrawableGroup::getHashHitDensity).thenComparing(Comparator.comparing(DrawableGroup::getUncategorizedCount)));
@Override
public Comparator<DrawableGroup> getGrpComparator(SortOrder sortOrder) {
return Comparator.nullsLast(Comparator.comparingDouble(DrawableGroup::getHashHitDensity).thenComparingInt(DrawableGroup::getSize).reversed());
}
@Override @Override
public <A extends Comparable<A>> Comparator<A> getValueComparator(DrawableAttribute<A> attr, SortOrder sortOrder) { public int compare(DrawableGroup o1, DrawableGroup o2) {
return getDefaultValueComparator(attr, sortOrder); 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 * get a list of the values of this enum
@ -108,8 +69,7 @@ public enum GroupSortBy implements ComparatorProvider {
* @return * @return
*/ */
public static ObservableList<GroupSortBy> getValues() { public static ObservableList<GroupSortBy> getValues() {
return FXCollections.observableArrayList(Arrays.asList(values())); return values;
} }
final private String displayName; final private String displayName;
@ -118,12 +78,12 @@ public enum GroupSortBy implements ComparatorProvider {
private final String imageName; 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.displayName = displayName;
this.sortOrderEnabled = sortOrderEnabled;
this.imageName = imagePath; this.imageName = imagePath;
this.delegate = internalComparator;
} }
public String getDisplayName() { public String getDisplayName() {
@ -139,49 +99,11 @@ public enum GroupSortBy implements ComparatorProvider {
return icon; return icon;
} }
public Boolean isSortOrderEnabled() { static class AllEqualComparator<A> implements Comparator<A> {
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> {
@Override @Override
public int compare(A o1, A o2) { public int compare(A o1, A o2) {
return 0; 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);
};
}
} }

View File

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

View File

@ -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);
}
}
}
}
}

View File

@ -1,16 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?> <?import javafx.geometry.Insets?>
<?import javafx.geometry.*?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.image.*?> <?import javafx.scene.image.Image?>
<?import javafx.scene.layout.*?> <?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> <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"> <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> <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> <children>
<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="NEVER"> <StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="NEVER">
<children> <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"> <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> <StackPane.margin>
<Insets left="3.0" right="3.0" /> <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> </children>
<HBox.margin> <HBox.margin>
<Insets /> <Insets />
@ -27,10 +34,13 @@
<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="NEVER"> <StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="NEVER">
<children> <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" /> <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> <StackPane.margin>
<Insets left="3.0" right="3.0" /> <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> </children>
<HBox.margin> <HBox.margin>
<Insets right="5.0" /> <Insets right="5.0" />
@ -42,19 +52,6 @@
</BorderPane.margin> </BorderPane.margin>
</HBox> </HBox>
</right> </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"> <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"> <graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image> <image>

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-14 Basis Technology Corp. * Copyright 2013-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org * Contact: carrier <at> sleuthkit <dot> org
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -19,8 +19,6 @@
package org.sleuthkit.autopsy.imagegallery.gui; package org.sleuthkit.autopsy.imagegallery.gui;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
@ -38,21 +36,12 @@ public class StatusBar extends AnchorPane {
private final ImageGalleryController controller; private final ImageGalleryController controller;
@FXML
private ResourceBundle resources;
@FXML
private URL location;
@FXML @FXML
private ProgressBar fileTaskProgresBar; private ProgressBar fileTaskProgresBar;
@FXML @FXML
private Label fileUpdateTaskLabel; private Label fileUpdateTaskLabel;
@FXML
private Label statusLabel;
@FXML @FXML
private Label bgTaskLabel; private Label bgTaskLabel;
@ -69,18 +58,11 @@ public class StatusBar extends AnchorPane {
void initialize() { void initialize() {
assert fileTaskProgresBar != null : "fx:id=\"fileTaskProgresBar\" was not injected: check your FXML file 'StatusBar.fxml'."; 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 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 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'."; 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"); fileUpdateTaskLabel.textProperty().bind(controller.getDBTasksQueueSizeProperty().asString().concat(Bundle.StatusBar_fileUpdateTaskLabel_text()));
fileTaskProgresBar.progressProperty().bind(controller.getFileUpdateQueueSizeProperty().negate()); fileTaskProgresBar.progressProperty().bind(controller.getDBTasksQueueSizeProperty().negate());
// controller.getFileUpdateQueueSizeProperty().addListener((ov, oldSize, newSize) -> {
// Platform.runLater(() -> {
//
//
// });
// });
controller.regroupProgress().addListener((ov, oldSize, newSize) -> { controller.regroupProgress().addListener((ov, oldSize, newSize) -> {
Platform.runLater(() -> { Platform.runLater(() -> {
@ -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()); 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();
}
} }

View File

@ -1,65 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.ComboBox?> <?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuItem?> <?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.Separator?> <?import javafx.scene.control.Separator?>
<?import javafx.scene.control.Slider?> <?import javafx.scene.control.Slider?>
<?import javafx.scene.control.SplitMenuButton?> <?import javafx.scene.control.SplitMenuButton?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.control.ToolBar?> <?import javafx.scene.control.ToolBar?>
<?import javafx.scene.image.Image?> <?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?> <?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?> <?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"> <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> <items>
<HBox alignment="CENTER" spacing="5.0">
<children>
<Label fx:id="groupByLabel" text="Group By:"> <Label fx:id="groupByLabel" text="Group By:">
<labelFor> <labelFor>
<ComboBox fx:id="groupByBox" editable="false" /> <ComboBox fx:id="groupByBox" editable="false" />
</labelFor> </labelFor>
</Label> </Label>
<fx:reference source="groupByBox" /> <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> </children>
</HBox> </HBox>
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" />
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="10.0" />
<HBox alignment="CENTER" spacing="5.0"> <HBox alignment="CENTER" spacing="5.0">
<children> <children>
<Label fx:id="tagImageViewLabel" text="Tag Group's Files:"> <Label fx:id="tagImageViewLabel" text="Tag Group's Files:">
@ -97,8 +67,11 @@
</items> </items>
</SplitMenuButton> </SplitMenuButton>
</children> </children>
<padding>
<Insets left="5.0" />
</padding>
</HBox> </HBox>
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" /> <Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="10.0" />
<HBox alignment="CENTER" spacing="5.0"> <HBox alignment="CENTER" spacing="5.0">
<children> <children>
<Label fx:id="thumbnailSizeLabel" text="Thumbnail Size (px):"> <Label fx:id="thumbnailSizeLabel" text="Thumbnail Size (px):">

View File

@ -32,14 +32,10 @@ import javafx.fxml.FXML;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Slider; import javafx.scene.control.Slider;
import javafx.scene.control.SplitMenuButton; import javafx.scene.control.SplitMenuButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.ToolBar; import javafx.scene.control.ToolBar;
import javafx.scene.layout.HBox;
import javax.swing.SortOrder; import javax.swing.SortOrder;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; 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.actions.TagGroupAction;
import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; 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.GroupSortBy;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
@ -67,21 +64,6 @@ public class Toolbar extends ToolBar {
@FXML @FXML
private Slider sizeSlider; 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 @FXML
private SplitMenuButton catGroupMenuButton; private SplitMenuButton catGroupMenuButton;
@ -91,9 +73,6 @@ public class Toolbar extends ToolBar {
@FXML @FXML
private Label groupByLabel; private Label groupByLabel;
@FXML
private Label sortByLabel;
@FXML @FXML
private Label tagImageViewLabel; private Label tagImageViewLabel;
@ -107,20 +86,18 @@ public class Toolbar extends ToolBar {
private final SimpleObjectProperty<SortOrder> orderProperty = new SimpleObjectProperty<>(SortOrder.ASCENDING); 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 final ImageGalleryController controller;
private SortChooser<DrawableGroup, GroupSortBy> sortChooser;
synchronized public SortOrder getSortOrder() { private final InvalidationListener queryInvalidationListener = new InvalidationListener() {
return orderProperty.get(); public void invalidated(Observable o) {
controller.getGroupManager().regroup(
groupByBox.getSelectionModel().getSelectedItem(),
sortChooser.getComparator(),
sortChooser.getSortOrder(),
false);
} }
};
public DoubleProperty sizeSliderValue() { public DoubleProperty sizeSliderValue() {
return sizeSlider.valueProperty(); return sizeSlider.valueProperty();
@ -142,15 +119,9 @@ public class Toolbar extends ToolBar {
"Toolbar.categoryImageViewLabel=Categorize Group's Files:", "Toolbar.categoryImageViewLabel=Categorize Group's Files:",
"Toolbar.thumbnailSizeLabel=Thumbnail Size (px):"}) "Toolbar.thumbnailSizeLabel=Thumbnail Size (px):"})
void initialize() { 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 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 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 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'."; assert tagGroupMenuButton != null : "fx:id=\"tagSelectedMenubutton\" was not injected: check your FXML file 'Toolbar.fxml'.";
controller.viewState().addListener((observable, oldViewState, newViewState) -> { 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); CategorizeGroupAction cat5GroupAction = new CategorizeGroupAction(Category.FIVE, controller);
catGroupMenuButton.setOnAction(cat5GroupAction); catGroupMenuButton.setOnAction(cat5GroupAction);
catGroupMenuButton.setText(cat5GroupAction.getText()); 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.setItems(FXCollections.observableList(DrawableAttribute.getGroupableAttrs()));
groupByBox.getSelectionModel().select(DrawableAttribute.PATH); groupByBox.getSelectionModel().select(DrawableAttribute.PATH);
groupByBox.getSelectionModel().selectedItemProperty().addListener(queryInvalidationListener); groupByBox.getSelectionModel().selectedItemProperty().addListener(queryInvalidationListener);
@ -201,21 +170,20 @@ public class Toolbar extends ToolBar {
groupByBox.setCellFactory(listView -> new AttributeListCell()); groupByBox.setCellFactory(listView -> new AttributeListCell());
groupByBox.setButtonCell(new AttributeListCell()); groupByBox.setButtonCell(new AttributeListCell());
sortByBox.setCellFactory(listView -> new SortByListCell()); sortChooser = new SortChooser<>(GroupSortBy.getValues());
sortByBox.setButtonCell(new SortByListCell()); sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> {
sortByBox.setItems(GroupSortBy.getValues()); final boolean orderEnabled = newComparator == GroupSortBy.NONE || newComparator == GroupSortBy.PRIORITY;
sortChooser.setSortOrderDisabled(orderEnabled);
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);
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) { private void syncGroupControlsEnabledState(GroupViewState newViewState) {
@ -230,8 +198,6 @@ public class Toolbar extends ToolBar {
public void reset() { public void reset() {
Platform.runLater(() -> { Platform.runLater(() -> {
groupByBox.getSelectionModel().select(DrawableAttribute.PATH); groupByBox.getSelectionModel().select(DrawableAttribute.PATH);
sortByBox.getSelectionModel().select(GroupSortBy.NONE);
orderGroup.selectToggle(ascRadio);
sizeSlider.setValue(SIZE_SLIDER_DEFAULT); sizeSlider.setValue(SIZE_SLIDER_DEFAULT);
}); });
} }

View File

@ -63,7 +63,7 @@ import org.sleuthkit.autopsy.directorytree.NewWindowViewAction;
import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel; import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; 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.CategorizeAction;
import org.sleuthkit.autopsy.imagegallery.actions.DeleteFollowUpTagAction; import org.sleuthkit.autopsy.imagegallery.actions.DeleteFollowUpTagAction;
import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction; import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction;
@ -182,10 +182,8 @@ public abstract class DrawableTileBase extends DrawableUIBase {
private ContextMenu buildContextMenu(DrawableFile<?> file) { private ContextMenu buildContextMenu(DrawableFile<?> file) {
final ArrayList<MenuItem> menuItems = new ArrayList<>(); final ArrayList<MenuItem> menuItems = new ArrayList<>();
menuItems.add(new CategorizeAction(getController()).getPopupMenu()); menuItems.add(CategorizeAction.getCategoriesMenu(getController()));
menuItems.add(AddTagAction.getTagMenu(getController()));
menuItems.add(new AddDrawableTagAction(getController()).getPopupMenu());
final MenuItem extractMenuItem = new MenuItem(Bundle.DrawableTileBase_menuItem_extractFiles()); final MenuItem extractMenuItem = new MenuItem(Bundle.DrawableTileBase_menuItem_extractFiles());
extractMenuItem.setOnAction(actionEvent -> { extractMenuItem.setOnAction(actionEvent -> {
@ -196,7 +194,6 @@ public abstract class DrawableTileBase extends DrawableUIBase {
}); });
menuItems.add(extractMenuItem); menuItems.add(extractMenuItem);
MenuItem contentViewer = new MenuItem(Bundle.DrawableTileBase_menuItem_showContentViewer()); MenuItem contentViewer = new MenuItem(Bundle.DrawableTileBase_menuItem_showContentViewer());
contentViewer.setOnAction(actionEvent -> { contentViewer.setOnAction(actionEvent -> {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
@ -242,7 +239,7 @@ public abstract class DrawableTileBase extends DrawableUIBase {
if (followUpToggle.isSelected() == true) { if (followUpToggle.isSelected() == true) {
try { try {
selectionModel.clearAndSelect(file.getId()); selectionModel.clearAndSelect(file.getId());
new AddDrawableTagAction(getController()).addTag(getController().getTagsManager().getFollowUpTagName(), ""); new AddTagAction(getController(), getController().getTagsManager().getFollowUpTagName(), selectionModel.getSelected()).handle(actionEvent);
} catch (TskCoreException ex) { } catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to add Follow Up tag. Could not load TagName.", ex); //NON-NLS LOGGER.log(Level.SEVERE, "Failed to add Follow Up tag. Could not load TagName.", ex); //NON-NLS
} }

View File

@ -19,6 +19,7 @@
package org.sleuthkit.autopsy.imagegallery.gui.drawableviews; package org.sleuthkit.autopsy.imagegallery.gui.drawableviews;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -47,6 +48,7 @@ import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.fxml.FXML; 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.FileIDSelectionModel;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; 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.Back;
import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction; import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction;
import org.sleuthkit.autopsy.imagegallery.actions.CategorizeSelectedFilesAction; import org.sleuthkit.autopsy.imagegallery.actions.CategorizeSelectedFilesAction;
@ -403,9 +405,9 @@ public class GroupPane extends BorderPane {
toggleForCategory.getStyleClass().add("toggle-button"); toggleForCategory.getStyleClass().add("toggle-button");
toggleForCategory.selectedProperty().addListener((ov, wasSelected, toggleSelected) -> { toggleForCategory.selectedProperty().addListener((ov, wasSelected, toggleSelected) -> {
if (toggleSelected && slideShowPane != null) { if (toggleSelected && slideShowPane != null) {
slideShowPane.getFileID().ifPresent((fileID) -> { slideShowPane.getFileID().ifPresent(fileID -> {
selectionModel.clearAndSelect(fileID); selectionModel.clearAndSelect(fileID);
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(cat), ""); new CategorizeAction(controller, cat, ImmutableSet.of(fileID)).handle(null);
}); });
} }
}); });
@ -762,35 +764,40 @@ public class GroupPane extends BorderPane {
selectAllFiles(); selectAllFiles();
t.consume(); t.consume();
} }
if (selectionModel.getSelected().isEmpty() == false) { ObservableSet<Long> selected = selectionModel.getSelected();
switch (t.getCode()) { 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 NUMPAD0:
case DIGIT0: case DIGIT0:
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.ZERO), ""); return Category.ZERO;
break;
case NUMPAD1: case NUMPAD1:
case DIGIT1: case DIGIT1:
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.ONE), ""); return Category.ONE;
break;
case NUMPAD2: case NUMPAD2:
case DIGIT2: case DIGIT2:
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.TWO), ""); return Category.TWO;
break;
case NUMPAD3: case NUMPAD3:
case DIGIT3: case DIGIT3:
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.THREE), ""); return Category.THREE;
break;
case NUMPAD4: case NUMPAD4:
case DIGIT4: case DIGIT4:
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.FOUR), ""); return Category.FOUR;
break;
case NUMPAD5: case NUMPAD5:
case DIGIT5: case DIGIT5:
new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(Category.FIVE), ""); return Category.FIVE;
break;
}
} }
} }
return null;
} }
private void handleArrows(KeyEvent t) { private void handleArrows(KeyEvent t) {
@ -826,8 +833,10 @@ public class GroupPane extends BorderPane {
private ContextMenu buildContextMenu() { private ContextMenu buildContextMenu() {
ArrayList<MenuItem> menuItems = new ArrayList<>(); 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); Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class);

View File

@ -1,4 +1,4 @@
.groupTreeCell{ .groupCell{
-fx-indent:5; /* default indent is 10 */ -fx-indent:5; /* default indent is 10 */
} }

View File

@ -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);
});
}
}
}

View File

@ -18,16 +18,13 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.gui.navpanel; package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
import com.google.common.collect.ImmutableList;
import java.util.Comparator; import java.util.Comparator;
import java.util.function.Function; import java.util.function.Function;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
/**
*
*/
@NbBundle.Messages({"GroupComparators.uncategorizedCount=Uncategorized Count", @NbBundle.Messages({"GroupComparators.uncategorizedCount=Uncategorized Count",
"GroupComparators.groupName=Group Name", "GroupComparators.groupName=Group Name",
"GroupComparators.hitCount=Hit Count", "GroupComparators.hitCount=Hit Count",
@ -50,10 +47,10 @@ final class GroupComparators<T extends Comparable<T>> implements Comparator<Draw
static final GroupComparators<Double> HIT_FILE_RATIO = static final GroupComparators<Double> HIT_FILE_RATIO =
new GroupComparators<>(Bundle.GroupComparators_hitDensity(), DrawableGroup::getHashHitDensity, density -> String.format("%.2f", density) + "%", true); //NON-NLS 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() { public static ObservableList<GroupComparators<?>> getValues() {
return values; return FXCollections.unmodifiableObservableList(values);
} }
private final Function<DrawableGroup, T> extractor; private final Function<DrawableGroup, T> extractor;

View File

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

View File

@ -72,7 +72,8 @@ final public class GroupTree extends NavPanel<TreeItem<GroupTreeNode>> {
getToolBar().visibleProperty().bind(groupedByPath.not()); getToolBar().visibleProperty().bind(groupedByPath.not());
getToolBar().managedProperty().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); groupTree.setShowRoot(false);
getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change<? extends DrawableGroup> change) -> { getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change<? extends DrawableGroup> change) -> {
@ -151,4 +152,5 @@ final public class GroupTree extends NavPanel<TreeItem<GroupTreeNode>> {
return Arrays.asList(stripStart); return Arrays.asList(stripStart);
} }
} }
} }

View File

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

View File

@ -79,8 +79,8 @@ final public class HashHitGroupList extends NavPanel<DrawableGroup> {
getBorderPane().setCenter(groupList); getBorderPane().setCenter(groupList);
sorted = getController().getGroupManager().getAnalyzedGroups().filtered((DrawableGroup t) -> t.getHashSetHitsCount() > 0).sorted(getDefaultComparator()); 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); groupList.setItems(sorted);
} }

View File

@ -1,63 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?> <?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.Tab?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.control.ToolBar?> <?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.BorderPane?>
<?import javafx.scene.layout.VBox?>
<fx:root closable="false" type="Tab" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
<fx:root type="Tab" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" closable="false" >
<content> <content>
<BorderPane prefHeight="200.0" prefWidth="200.0" fx:id="borderPane"> <BorderPane fx:id="borderPane">
<top> <top>
<ToolBar fx:id="toolBar" BorderPane.alignment="CENTER"> <ToolBar fx:id="toolBar" minWidth="-Infinity" 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>
</top> </top>
</BorderPane> </BorderPane>
</content> </content>

View File

@ -22,15 +22,13 @@ import com.google.common.eventbus.Subscribe;
import java.util.Comparator; import java.util.Comparator;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML; 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.SelectionModel;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.ToolBar; import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javax.swing.SortOrder;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; 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.DrawableGroup;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; 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. * Base class for Tabs in the left hand Navigation/Context area.
@ -50,24 +49,10 @@ abstract class NavPanel<X> extends Tab {
@FXML @FXML
private ToolBar toolBar; 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 ImageGalleryController controller;
private final GroupManager groupManager; private final GroupManager groupManager;
private final CategoryManager categoryManager; private final CategoryManager categoryManager;
private SortChooser<DrawableGroup, GroupComparators<?>> sortChooser;
NavPanel(ImageGalleryController controller) { NavPanel(ImageGalleryController controller) {
this.controller = controller; this.controller = controller;
@ -75,6 +60,10 @@ abstract class NavPanel<X> extends Tab {
this.categoryManager = controller.getCategoryManager(); this.categoryManager = controller.getCategoryManager();
} }
public ReadOnlyObjectProperty<GroupComparators<?>> comparatorProperty() {
return sortChooser.comparatorProperty();
}
@FXML @FXML
@NbBundle.Messages({"NavPanel.ascRadio.text=Ascending", @NbBundle.Messages({"NavPanel.ascRadio.text=Ascending",
"NavPanel.descRadio.text=Descending", "NavPanel.descRadio.text=Descending",
@ -82,27 +71,24 @@ abstract class NavPanel<X> extends Tab {
void initialize() { void initialize() {
assert borderPane != null : "fx:id=\"borderPane\" was not injected: check your FXML file 'NavPanel.fxml'."; assert borderPane != null : "fx:id=\"borderPane\" was not injected: check your FXML file 'NavPanel.fxml'.";
assert toolBar != null : "fx:id=\"toolBar\" was not injected: check your FXML file 'NavPanel.fxml'."; assert 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()); sortChooser = new SortChooser<>(GroupComparators.getValues());
sortByBox.getSelectionModel().select(getDefaultComparator()); sortChooser.setComparator(getDefaultComparator());
orderGroup.selectedToggleProperty().addListener(order -> sortGroups()); sortChooser.sortOrderProperty().addListener(order -> sortGroups());
sortByBox.getSelectionModel().selectedItemProperty().addListener(observable -> { sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> {
sortGroups(); sortGroups();
//only need to listen to changes in category if we are sorting by/ showing the uncategorized count //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); categoryManager.registerListener(NavPanel.this);
} else { } else {
categoryManager.unregisterListener(NavPanel.this); categoryManager.unregisterListener(NavPanel.this);
} }
});
ascRadio.setText(Bundle.NavPanel_ascRadio_text()); final SortChooser.ValueType valueType = newComparator == GroupComparators.ALPHABETICAL ? SortChooser.ValueType.LEXICOGRAPHIC : SortChooser.ValueType.NUMERIC;
descRadio.setText(Bundle.NavPanel_descRadio_text()); sortChooser.setValueType(valueType);
sortByBoxLabel.setText(Bundle.NavPanel_sortByBoxLabel_text()); });
toolBar.getItems().add(sortChooser);
//keep selection in sync with controller //keep selection in sync with controller
controller.viewState().addListener(observable -> { controller.viewState().addListener(observable -> {
Optional.ofNullable(controller.viewState().get()) Optional.ofNullable(controller.viewState().get())
@ -129,8 +115,8 @@ abstract class NavPanel<X> extends Tab {
*/ */
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
Comparator<DrawableGroup> getComparator() { Comparator<DrawableGroup> getComparator() {
Comparator<DrawableGroup> comparator = sortByBox.getSelectionModel().getSelectedItem(); Comparator<DrawableGroup> comparator = sortChooser.getComparator();
return (orderGroup.getSelectedToggle() == ascRadio) return (sortChooser.getSortOrder() == SortOrder.ASCENDING)
? comparator ? comparator
: comparator.reversed(); : comparator.reversed();
} }
@ -194,22 +180,6 @@ abstract class NavPanel<X> extends Tab {
return toolBar; return toolBar;
} }
ComboBox<GroupComparators<?>> getSortByBox() {
return sortByBox;
}
RadioButton getAscRadio() {
return ascRadio;
}
ToggleGroup getOrderGroup() {
return orderGroup;
}
RadioButton getDescRadio() {
return descRadio;
}
ImageGalleryController getController() { ImageGalleryController getController() {
return controller; 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