Merge remote-tracking branch 'upstream/develop' into aut-2034

This commit is contained in:
Oliver Spohngellert 2016-04-13 13:59:48 -04:00
commit c8a8c7815e
77 changed files with 3631 additions and 2409 deletions

View File

@ -604,7 +604,7 @@ public class Case implements SleuthkitCase.ErrorObserver {
SleuthkitCase db; SleuthkitCase db;
String caseDir; String caseDir;
if (caseType == CaseType.SINGLE_USER_CASE) { if (caseType == CaseType.SINGLE_USER_CASE) {
String dbPath = metadata.getCaseDatabase(); //NON-NLS String dbPath = metadata.getCaseDatabasePath(); //NON-NLS
db = SleuthkitCase.openCase(dbPath); db = SleuthkitCase.openCase(dbPath);
caseDir = new File(dbPath).getParent(); caseDir = new File(dbPath).getParent();
} else { } else {
@ -612,7 +612,7 @@ public class Case implements SleuthkitCase.ErrorObserver {
throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.open.exception.multiUserCaseNotEnabled")); throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.open.exception.multiUserCaseNotEnabled"));
} }
try { try {
db = SleuthkitCase.openCase(metadata.getCaseDatabase(), UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory()); db = SleuthkitCase.openCase(metadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory());
} catch (UserPreferencesException ex) { } catch (UserPreferencesException ex) {
throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex); throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex);
} }

View File

@ -39,6 +39,7 @@ import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource; import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamResult;
import org.sleuthkit.autopsy.coreutils.Version;
import org.sleuthkit.autopsy.coreutils.XMLUtil; import org.sleuthkit.autopsy.coreutils.XMLUtil;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
@ -53,22 +54,23 @@ public final class CaseMetadata {
private static final String FILE_EXTENSION = ".aut"; private static final String FILE_EXTENSION = ".aut";
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss (z)"); private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss (z)");
private static final String SCHEMA_VERSION_ONE = "1.0"; private static final String SCHEMA_VERSION_ONE = "1.0";
private final static String CREATED_BY_VERSION_ELEMENT_NAME = "AutopsyCreatedVersion"; //NON-NLS private final static String AUTOPSY_CREATED_VERSION_ELEMENT_NAME = "AutopsyCreatedVersion"; //NON-NLS
private final static String CASE_DATABASE_NAME_ELEMENT_NAME = "DatabaseName"; //NON-NLS private final static String CASE_DATABASE_NAME_ELEMENT_NAME = "DatabaseName"; //NON-NLS
private final static String TEXT_INDEX_NAME_ELEMENT = "TextIndexName"; //NON-NLS
private static final String CURRENT_SCHEMA_VERSION = "2.0"; private static final String CURRENT_SCHEMA_VERSION = "2.0";
private final static String ROOT_ELEMENT_NAME = "AutopsyCase"; //NON-NLS private final static String ROOT_ELEMENT_NAME = "AutopsyCase"; //NON-NLS
private final static String SCHEMA_VERSION_ELEMENT_NAME = "SchemaVersion"; //NON-NLS private final static String SCHEMA_VERSION_ELEMENT_NAME = "SchemaVersion"; //NON-NLS
private final static String CREATED_DATE_ELEMENT_NAME = "CreatedDate"; //NON-NLS private final static String CREATED_DATE_ELEMENT_NAME = "CreatedDate"; //NON-NLS
private final static String MODIFIED_DATE_ELEMENT_NAME = "ModifiedDate"; //NON-NLS private final static String MODIFIED_DATE_ELEMENT_NAME = "ModifiedDate"; //NON-NLS
private final static String CREATED_BY_BUILD_ELEMENT_NAME = "CreatedByBuild"; //NON-NLS private final static String AUTOPSY_CREATED_BY_ELEMENT_NAME = "CreatedByAutopsyVersion"; //NON-NLS
private final static String SAVED_BY_BUILD_ELEMENT_NAME = "SavedByBuild"; //NON-NLS private final static String AUTOPSY_SAVED_BY_ELEMENT_NAME = "SavedByAutopsyVersion"; //NON-NLS
private final static String CASE_ELEMENT_NAME = "Case"; //NON-NLS private final static String CASE_ELEMENT_NAME = "Case"; //NON-NLS
private final static String CASE_NAME_ELEMENT_NAME = "Name"; //NON-NLS private final static String CASE_NAME_ELEMENT_NAME = "Name"; //NON-NLS
private final static String CASE_NUMBER_ELEMENT_NAME = "Number"; //NON-NLS private final static String CASE_NUMBER_ELEMENT_NAME = "Number"; //NON-NLS
private final static String EXAMINER_ELEMENT_NAME = "Examiner"; //NON-NLS private final static String EXAMINER_ELEMENT_NAME = "Examiner"; //NON-NLS
private final static String CASE_TYPE_ELEMENT_NAME = "CaseType"; //NON-NLS private final static String CASE_TYPE_ELEMENT_NAME = "CaseType"; //NON-NLS
private final static String CASE_DATABASE_ELEMENT_NAME = "Database"; //NON-NLS private final static String CASE_DATABASE_ELEMENT_NAME = "Database"; //NON-NLS
private final static String TEXT_INDEX_NAME_ELEMENT = "TextIndexName"; //NON-NLS private final static String TEXT_INDEX_ELEMENT = "TextIndex"; //NON-NLS
private final Path metadataFilePath; private final Path metadataFilePath;
private Case.CaseType caseType; private Case.CaseType caseType;
private String caseName; private String caseName;
@ -77,7 +79,7 @@ public final class CaseMetadata {
private String caseDatabase; private String caseDatabase;
private String textIndexName; private String textIndexName;
private String createdDate; private String createdDate;
private String createdByBuild; private String createdByVersion;
/** /**
* Gets the file extension used for case metadata files. * Gets the file extension used for case metadata files.
@ -90,7 +92,7 @@ public final class CaseMetadata {
/** /**
* Constructs an object that provides access to the case metadata stored in * Constructs an object that provides access to the case metadata stored in
* a new case metadata file. * a new case metadata file that is created using the supplied metadata.
* *
* @param caseDirectory The case directory. * @param caseDirectory The case directory.
* @param caseType The type of case. * @param caseType The type of case.
@ -113,7 +115,7 @@ public final class CaseMetadata {
this.examiner = examiner; this.examiner = examiner;
this.caseDatabase = caseDatabase; this.caseDatabase = caseDatabase;
this.textIndexName = caseTextIndexName; this.textIndexName = caseTextIndexName;
createdByBuild = System.getProperty("netbeans.buildnumber"); createdByVersion = Version.getVersion();
createdDate = CaseMetadata.DATE_FORMAT.format(new Date()); createdDate = CaseMetadata.DATE_FORMAT.format(new Date());
writeToFile(); writeToFile();
} }
@ -147,7 +149,7 @@ public final class CaseMetadata {
* @return The case directory. * @return The case directory.
*/ */
public String getCaseDirectory() { public String getCaseDirectory() {
return this.metadataFilePath.getParent().toString(); return metadataFilePath.getParent().toString();
} }
/** /**
@ -156,7 +158,7 @@ public final class CaseMetadata {
* @return The case type. * @return The case type.
*/ */
public Case.CaseType getCaseType() { public Case.CaseType getCaseType() {
return this.caseType; return caseType;
} }
/** /**
@ -175,10 +177,10 @@ public final class CaseMetadata {
* @param caseName A case display name. * @param caseName A case display name.
*/ */
void setCaseName(String caseName) throws CaseMetadataException { void setCaseName(String caseName) throws CaseMetadataException {
String oldCaseName = this.caseName; String oldCaseName = caseName;
this.caseName = caseName; this.caseName = caseName;
try { try {
this.writeToFile(); writeToFile();
} catch (CaseMetadataException ex) { } catch (CaseMetadataException ex) {
this.caseName = oldCaseName; this.caseName = oldCaseName;
throw ex; throw ex;
@ -204,13 +206,32 @@ public final class CaseMetadata {
} }
/** /**
* Gets a string identifying the case database. * Gets the name of the case case database.
* *
* @return For a single-user case, the full path to the case database file. * @return The case database name.
* For a multi-user case, the case database name.
*/ */
public String getCaseDatabase() { public String getCaseDatabaseName() {
if (caseType == Case.CaseType.MULTI_USER_CASE) {
return caseDatabase; return caseDatabase;
} else {
return Paths.get(caseDatabase).getFileName().toString();
}
}
/**
* Gets the full path to the case database file if the case is a single-user
* case.
*
* @return The full path to the case database file for a single-user case.
*
* @throws UnsupportedOperationException If called for a multi-user case.
*/
public String getCaseDatabasePath() throws UnsupportedOperationException {
if (caseType == Case.CaseType.SINGLE_USER_CASE) {
return caseDatabase;
} else {
throw new UnsupportedOperationException();
}
} }
/** /**
@ -228,7 +249,7 @@ public final class CaseMetadata {
* @return The date this case was created as a string * @return The date this case was created as a string
*/ */
String getCreatedDate() { String getCreatedDate() {
return this.createdDate; return createdDate;
} }
/** /**
@ -238,10 +259,10 @@ public final class CaseMetadata {
* @param createdDate The date the case was created as a string. * @param createdDate The date the case was created as a string.
*/ */
void setCreatedDate(String createdDate) throws CaseMetadataException { void setCreatedDate(String createdDate) throws CaseMetadataException {
String oldCreatedDate = this.createdDate; String oldCreatedDate = createdDate;
this.createdDate = createdDate; this.createdDate = createdDate;
try { try {
this.writeToFile(); writeToFile();
} catch (CaseMetadataException ex) { } catch (CaseMetadataException ex) {
this.createdDate = oldCreatedDate; this.createdDate = oldCreatedDate;
throw ex; throw ex;
@ -249,27 +270,27 @@ public final class CaseMetadata {
} }
/** /**
* Gets the build that created the case. * Gets the Autopsy version that created the case.
* *
* @return A build identifier. * @return A build identifier.
*/ */
String getCreatedByBuild() { String getCreatedByVersion() {
return createdByBuild; return createdByVersion;
} }
/** /**
* Gets the build that created the case. Used for preserving the case * Sets the Autopsy version that created the case. Used for preserving this
* creation build during single-user to multi-user case conversion. * metadata during single-user to multi-user case conversion.
* *
* @param buildVersion An build version identifier. * @param buildVersion An build version identifier.
*/ */
void setCreatedByBuild(String buildVersion) throws CaseMetadataException { void setCreatedByVersion(String buildVersion) throws CaseMetadataException {
String oldCreatedByVersion = this.createdByBuild; String oldCreatedByVersion = this.createdByVersion;
this.createdByBuild = buildVersion; this.createdByVersion = buildVersion;
try { try {
this.writeToFile(); this.writeToFile();
} catch (CaseMetadataException ex) { } catch (CaseMetadataException ex) {
this.createdByBuild = oldCreatedByVersion; this.createdByVersion = oldCreatedByVersion;
throw ex; throw ex;
} }
} }
@ -323,22 +344,22 @@ public final class CaseMetadata {
Element rootElement = doc.createElement(ROOT_ELEMENT_NAME); Element rootElement = doc.createElement(ROOT_ELEMENT_NAME);
doc.appendChild(rootElement); doc.appendChild(rootElement);
createChildElement(doc, rootElement, SCHEMA_VERSION_ELEMENT_NAME, CURRENT_SCHEMA_VERSION); createChildElement(doc, rootElement, SCHEMA_VERSION_ELEMENT_NAME, CURRENT_SCHEMA_VERSION);
createChildElement(doc, rootElement, CREATED_DATE_ELEMENT_NAME, this.getCreatedDate()); createChildElement(doc, rootElement, CREATED_DATE_ELEMENT_NAME, createdDate);
createChildElement(doc, rootElement, MODIFIED_DATE_ELEMENT_NAME, DATE_FORMAT.format(new Date())); createChildElement(doc, rootElement, MODIFIED_DATE_ELEMENT_NAME, DATE_FORMAT.format(new Date()));
createChildElement(doc, rootElement, CREATED_BY_BUILD_ELEMENT_NAME, this.getCreatedByBuild()); createChildElement(doc, rootElement, AUTOPSY_CREATED_BY_ELEMENT_NAME, createdByVersion);
createChildElement(doc, rootElement, SAVED_BY_BUILD_ELEMENT_NAME, System.getProperty("netbeans.buildnumber")); createChildElement(doc, rootElement, AUTOPSY_SAVED_BY_ELEMENT_NAME, Version.getVersion());
Element caseElement = doc.createElement(CASE_ELEMENT_NAME); Element caseElement = doc.createElement(CASE_ELEMENT_NAME);
rootElement.appendChild(caseElement); rootElement.appendChild(caseElement);
/* /*
* Create the children of the case element. * Create the children of the case element.
*/ */
createChildElement(doc, caseElement, CASE_NAME_ELEMENT_NAME, this.getCaseName()); createChildElement(doc, caseElement, CASE_NAME_ELEMENT_NAME, caseName);
createChildElement(doc, caseElement, CASE_NUMBER_ELEMENT_NAME, this.getCaseNumber()); createChildElement(doc, caseElement, CASE_NUMBER_ELEMENT_NAME, caseNumber);
createChildElement(doc, caseElement, EXAMINER_ELEMENT_NAME, this.getExaminer()); createChildElement(doc, caseElement, EXAMINER_ELEMENT_NAME, examiner);
createChildElement(doc, caseElement, CASE_TYPE_ELEMENT_NAME, this.getCaseType().toString()); createChildElement(doc, caseElement, CASE_TYPE_ELEMENT_NAME, caseType.toString());
createChildElement(doc, caseElement, CASE_DATABASE_ELEMENT_NAME, this.getCaseDatabaseName()); createChildElement(doc, caseElement, CASE_DATABASE_ELEMENT_NAME, caseDatabase);
createChildElement(doc, caseElement, TEXT_INDEX_NAME_ELEMENT, this.getTextIndexName()); createChildElement(doc, caseElement, TEXT_INDEX_ELEMENT, textIndexName);
} }
/** /**
@ -381,9 +402,9 @@ public final class CaseMetadata {
String schemaVersion = getElementTextContent(rootElement, SCHEMA_VERSION_ELEMENT_NAME, true); String schemaVersion = getElementTextContent(rootElement, SCHEMA_VERSION_ELEMENT_NAME, true);
this.createdDate = getElementTextContent(rootElement, CREATED_DATE_ELEMENT_NAME, true); this.createdDate = getElementTextContent(rootElement, CREATED_DATE_ELEMENT_NAME, true);
if (schemaVersion.equals(SCHEMA_VERSION_ONE)) { if (schemaVersion.equals(SCHEMA_VERSION_ONE)) {
this.createdByBuild = getElementTextContent(rootElement, CREATED_BY_VERSION_ELEMENT_NAME, true); this.createdByVersion = getElementTextContent(rootElement, AUTOPSY_CREATED_VERSION_ELEMENT_NAME, true);
} else { } else {
this.createdByBuild = getElementTextContent(rootElement, CREATED_BY_BUILD_ELEMENT_NAME, true); this.createdByVersion = getElementTextContent(rootElement, AUTOPSY_CREATED_BY_ELEMENT_NAME, true);
} }
/* /*
@ -403,10 +424,11 @@ public final class CaseMetadata {
} }
if (schemaVersion.equals(SCHEMA_VERSION_ONE)) { if (schemaVersion.equals(SCHEMA_VERSION_ONE)) {
this.caseDatabase = getElementTextContent(caseElement, CASE_DATABASE_NAME_ELEMENT_NAME, true); this.caseDatabase = getElementTextContent(caseElement, CASE_DATABASE_NAME_ELEMENT_NAME, true);
this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_NAME_ELEMENT, true);
} else { } else {
this.caseDatabase = getElementTextContent(caseElement, CASE_DATABASE_ELEMENT_NAME, true); this.caseDatabase = getElementTextContent(caseElement, CASE_DATABASE_ELEMENT_NAME, true);
this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, true);
} }
this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_NAME_ELEMENT, true);
/* /*
* Update the file to the current schema, if necessary. * Update the file to the current schema, if necessary.
@ -461,17 +483,4 @@ public final class CaseMetadata {
} }
} }
/**
* Gets a string identifying the case database.
*
* @return For a single-user case, the full path to the case database file.
* For a multi-user case, the case database name.
*
* @deprecated Use getCaseDatabase instead.
*/
@Deprecated
public String getCaseDatabaseName() {
return getCaseDatabase();
}
} }

View File

@ -88,7 +88,11 @@ class CasePropertiesForm extends javax.swing.JPanel {
current = currentCase; current = currentCase;
CaseMetadata caseMetadata = currentCase.getCaseMetadata(); CaseMetadata caseMetadata = currentCase.getCaseMetadata();
tbDbName.setText(caseMetadata.getCaseDatabase()); if (caseMetadata.getCaseType() == Case.CaseType.SINGLE_USER_CASE) {
tbDbName.setText(caseMetadata.getCaseDatabasePath());
} else {
tbDbName.setText(caseMetadata.getCaseDatabaseName());
}
Case.CaseType caseType = caseMetadata.getCaseType(); Case.CaseType caseType = caseMetadata.getCaseType();
tbDbType.setText(caseType.getLocalizedDisplayName()); tbDbType.setText(caseType.getLocalizedDisplayName());
if (caseType == Case.CaseType.SINGLE_USER_CASE) { if (caseType == Case.CaseType.SINGLE_USER_CASE) {

View File

@ -198,7 +198,7 @@ public class SingleUserCaseConverter {
dbName, solrName); dbName, solrName);
// Set created date. This calls writefile, no need to call it again // Set created date. This calls writefile, no need to call it again
newCaseMetadata.setCreatedDate(oldCaseMetadata.getCreatedDate()); newCaseMetadata.setCreatedDate(oldCaseMetadata.getCreatedDate());
newCaseMetadata.setCreatedByBuild(oldCaseMetadata.getCreatedByBuild()); newCaseMetadata.setCreatedByVersion(oldCaseMetadata.getCreatedByVersion());
// At this point the import has been finished successfully so we can delete the original case // At this point the import has been finished successfully so we can delete the original case
// (if requested). This *should* be fairly safe - at this point we know there was an autopsy file // (if requested). This *should* be fairly safe - at this point we know there was an autopsy file

View File

@ -47,6 +47,7 @@ import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.concurrent.Worker; import javafx.concurrent.Worker;
import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.GuardedBy;
@ -73,6 +74,7 @@ import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.db.EventsRepository; import org.sleuthkit.autopsy.timeline.db.EventsRepository;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
@ -290,6 +292,21 @@ public class TimeLineController {
advance(filteredEvents.zoomParametersProperty().get().withTimeRange(boundingEventsInterval)); advance(filteredEvents.zoomParametersProperty().get().withTimeRange(boundingEventsInterval));
} }
private final ObservableSet<TimeLineEvent> pinnedEvents = FXCollections.observableSet();
private final ObservableSet<TimeLineEvent> pinnedEventsUnmodifiable = FXCollections.unmodifiableObservableSet(pinnedEvents);
public void pinEvent(TimeLineEvent event) {
pinnedEvents.add(event);
}
public void unPinEvent(TimeLineEvent event) {
pinnedEvents.removeIf(event::equals);
}
public ObservableSet<TimeLineEvent> getPinnedEvents() {
return pinnedEventsUnmodifiable;
}
/** /**
* rebuild the repo using the given repo builder (expected to be a member * rebuild the repo using the given repo builder (expected to be a member
* reference to {@link EventsRepository#rebuildRepository(java.util.function.Consumer) * reference to {@link EventsRepository#rebuildRepository(java.util.function.Consumer)

View File

@ -28,14 +28,18 @@ import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
/** /**
* Action that resets the filters to their initial/default state. * Action that resets the filters to their initial/default state.
*/ */
@NbBundle.Messages({"ResetFilters.text=Reset all filters",
"RestFilters.longText=Reset all filters to their default state."})
public class ResetFilters extends Action { public class ResetFilters extends Action {
private FilteredEventsModel eventsModel; private FilteredEventsModel eventsModel;
@NbBundle.Messages({"ResetFilters.text=Reset all filters",
"RestFilters.longText=Reset all filters to their default state."})
public ResetFilters(final TimeLineController controller) { public ResetFilters(final TimeLineController controller) {
super(Bundle.ResetFilters_text()); this(Bundle.ResetFilters_text(), controller);
}
public ResetFilters(String text, TimeLineController controller) {
super(text);
setLongText(Bundle.RestFilters_longText()); setLongText(Bundle.RestFilters_longText());
eventsModel = controller.getEventsModel(); eventsModel = controller.getEventsModel();
disabledProperty().bind(new BooleanBinding() { disabledProperty().bind(new BooleanBinding() {

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-15 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");
@ -33,12 +33,12 @@ import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
/** /**
* Represents a set of other (TimeLineEvent) events clustered together. All the * Represents a set of other events clustered together. All the sub events
* sub events should have the same type and matching descriptions at the * should have the same type and matching descriptions at the designated "zoom
* designated 'zoom level', and be 'close together' in time. * level", and be "close together" in time.
*/ */
@Immutable @Immutable
public class EventCluster implements EventBundle<EventStripe> { public class EventCluster implements MultiEvent<EventStripe> {
/** /**
* merge two event clusters into one new event cluster. * merge two event clusters into one new event cluster.
@ -57,11 +57,16 @@ public class EventCluster implements EventBundle<EventStripe> {
if (!cluster1.getDescription().equals(cluster2.getDescription())) { if (!cluster1.getDescription().equals(cluster2.getDescription())) {
throw new IllegalArgumentException("event clusters are not compatible: they have different descriptions"); throw new IllegalArgumentException("event clusters are not compatible: they have different descriptions");
} }
Sets.SetView<Long> idsUnion = Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs()); Sets.SetView<Long> idsUnion =
Sets.SetView<Long> hashHitsUnion = Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits()); Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs());
Sets.SetView<Long> taggedUnion = Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags()); Sets.SetView<Long> hashHitsUnion =
Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits());
Sets.SetView<Long> taggedUnion =
Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags());
return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod); return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span),
cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion,
cluster1.getDescription(), cluster1.lod);
} }
final private EventStripe parent; final private EventStripe parent;
@ -103,7 +108,9 @@ public class EventCluster implements EventBundle<EventStripe> {
*/ */
private final ImmutableSet<Long> hashHits; private final ImmutableSet<Long> hashHits;
private EventCluster(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLoD lod, EventStripe parent) { private EventCluster(Interval spanningInterval, EventType type, Set<Long> eventIDs,
Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLoD lod,
EventStripe parent) {
this.span = spanningInterval; this.span = spanningInterval;
this.type = type; this.type = type;
@ -115,12 +122,13 @@ public class EventCluster implements EventBundle<EventStripe> {
this.parent = parent; this.parent = parent;
} }
public EventCluster(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLoD lod) { public EventCluster(Interval spanningInterval, EventType type, Set<Long> eventIDs,
Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLoD lod) {
this(spanningInterval, type, eventIDs, hashHits, tagged, description, lod, null); this(spanningInterval, type, eventIDs, hashHits, tagged, description, lod, null);
} }
@Override @Override
public Optional<EventStripe> getParentBundle() { public Optional<EventStripe> getParent() {
return Optional.ofNullable(parent); return Optional.ofNullable(parent);
} }
@ -139,19 +147,16 @@ public class EventCluster implements EventBundle<EventStripe> {
} }
@Override @Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ImmutableSet<Long> getEventIDs() { public ImmutableSet<Long> getEventIDs() {
return eventIDs; return eventIDs;
} }
@Override @Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ImmutableSet<Long> getEventIDsWithHashHits() { public ImmutableSet<Long> getEventIDsWithHashHits() {
return hashHits; return hashHits;
} }
@Override @Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ImmutableSet<Long> getEventIDsWithTags() { public ImmutableSet<Long> getEventIDsWithTags() {
return tagged; return tagged;
} }
@ -181,9 +186,6 @@ public class EventCluster implements EventBundle<EventStripe> {
* EventBundle as the parent. * EventBundle as the parent.
*/ */
public EventCluster withParent(EventStripe parent) { public EventCluster withParent(EventStripe parent) {
if (Objects.nonNull(this.parent)) {
throw new IllegalStateException("Event Cluster already has a parent!");
}
return new EventCluster(span, type, eventIDs, hashHits, tagged, description, lod, parent); return new EventCluster(span, type, eventIDs, hashHits, tagged, description, lod, parent);
} }
@ -192,4 +194,45 @@ public class EventCluster implements EventBundle<EventStripe> {
return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(this).build(); return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(this).build();
} }
@Override
public String toString() {
return "EventCluster{" + "description=" + description + ", eventIDs=" + eventIDs.size() + '}';
}
@Override
public int hashCode() {
int hash = 7;
hash = 23 * hash + Objects.hashCode(this.type);
hash = 23 * hash + Objects.hashCode(this.description);
hash = 23 * hash + Objects.hashCode(this.lod);
hash = 23 * hash + Objects.hashCode(this.eventIDs);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final EventCluster other = (EventCluster) obj;
if (!Objects.equals(this.description, other.description)) {
return false;
}
if (!Objects.equals(this.type, other.type)) {
return false;
}
if (this.lod != other.lod) {
return false;
}
if (!Objects.equals(this.eventIDs, other.eventIDs)) {
return false;
}
return true;
}
} }

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");
@ -22,10 +22,10 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
import java.util.Comparator; import java.util.Comparator;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.SortedSet; import java.util.SortedSet;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import org.python.google.common.base.Objects;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
@ -34,15 +34,15 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
* description, and zoom levels, but not necessarily close together in time. * description, and zoom levels, but not necessarily close together in time.
*/ */
@Immutable @Immutable
public final class EventStripe implements EventBundle<EventCluster> { public final class EventStripe implements MultiEvent<EventCluster> {
public static EventStripe merge(EventStripe u, EventStripe v) { public static EventStripe merge(EventStripe u, EventStripe v) {
Preconditions.checkNotNull(u); Preconditions.checkNotNull(u);
Preconditions.checkNotNull(v); Preconditions.checkNotNull(v);
Preconditions.checkArgument(Objects.equal(u.description, v.description)); Preconditions.checkArgument(Objects.equals(u.description, v.description));
Preconditions.checkArgument(Objects.equal(u.lod, v.lod)); Preconditions.checkArgument(Objects.equals(u.lod, v.lod));
Preconditions.checkArgument(Objects.equal(u.type, v.type)); Preconditions.checkArgument(Objects.equals(u.type, v.type));
Preconditions.checkArgument(Objects.equal(u.parent, v.parent)); Preconditions.checkArgument(Objects.equals(u.parent, v.parent));
return new EventStripe(u, v); return new EventStripe(u, v);
} }
@ -82,8 +82,10 @@ public final class EventStripe implements EventBundle<EventCluster> {
private final ImmutableSet<Long> hashHits; private final ImmutableSet<Long> hashHits;
public EventStripe withParent(EventCluster parent) { public EventStripe withParent(EventCluster parent) {
EventStripe eventStripe = new EventStripe(parent, this.type, this.description, this.lod, clusters, eventIDs, tagged, hashHits); if (java.util.Objects.nonNull(this.parent)) {
return eventStripe; throw new IllegalStateException("Event Stripe already has a parent!");
}
return new EventStripe(parent, this.type, this.description, this.lod, clusters, eventIDs, tagged, hashHits);
} }
private EventStripe(EventCluster parent, EventType type, String description, DescriptionLoD lod, SortedSet<EventCluster> clusters, ImmutableSet<Long> eventIDs, ImmutableSet<Long> tagged, ImmutableSet<Long> hashHits) { private EventStripe(EventCluster parent, EventType type, String description, DescriptionLoD lod, SortedSet<EventCluster> clusters, ImmutableSet<Long> eventIDs, ImmutableSet<Long> tagged, ImmutableSet<Long> hashHits) {
@ -98,9 +100,10 @@ public final class EventStripe implements EventBundle<EventCluster> {
this.hashHits = hashHits; this.hashHits = hashHits;
} }
public EventStripe(EventCluster cluster, EventCluster parent) { public EventStripe(EventCluster cluster) {
this.clusters = ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)) this.clusters = ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis))
.add(cluster).build(); .add(cluster.withParent(this)).build();
type = cluster.getEventType(); type = cluster.getEventType();
description = cluster.getDescription(); description = cluster.getDescription();
@ -108,7 +111,7 @@ public final class EventStripe implements EventBundle<EventCluster> {
eventIDs = cluster.getEventIDs(); eventIDs = cluster.getEventIDs();
tagged = cluster.getEventIDsWithTags(); tagged = cluster.getEventIDsWithTags();
hashHits = cluster.getEventIDsWithHashHits(); hashHits = cluster.getEventIDsWithHashHits();
this.parent = parent; this.parent = null;
} }
private EventStripe(EventStripe u, EventStripe v) { private EventStripe(EventStripe u, EventStripe v) {
@ -132,14 +135,22 @@ public final class EventStripe implements EventBundle<EventCluster> {
.addAll(u.getEventIDsWithHashHits()) .addAll(u.getEventIDsWithHashHits())
.addAll(v.getEventIDsWithHashHits()) .addAll(v.getEventIDsWithHashHits())
.build(); .build();
parent = u.getParentBundle().orElse(v.getParentBundle().orElse(null)); parent = u.getParent().orElse(v.getParent().orElse(null));
} }
@Override @Override
public Optional<EventCluster> getParentBundle() { public Optional<EventCluster> getParent() {
return Optional.ofNullable(parent); return Optional.ofNullable(parent);
} }
public Optional<EventStripe> getParentStripe() {
if (getParent().isPresent()) {
return getParent().get().getParent();
} else {
return Optional.empty();
}
}
@Override @Override
public String getDescription() { public String getDescription() {
return description; return description;
@ -156,19 +167,16 @@ public final class EventStripe implements EventBundle<EventCluster> {
} }
@Override @Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ImmutableSet<Long> getEventIDs() { public ImmutableSet<Long> getEventIDs() {
return eventIDs; return eventIDs;
} }
@Override @Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ImmutableSet<Long> getEventIDsWithHashHits() { public ImmutableSet<Long> getEventIDsWithHashHits() {
return hashHits; return hashHits;
} }
@Override @Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ImmutableSet<Long> getEventIDsWithTags() { public ImmutableSet<Long> getEventIDsWithTags() {
return tagged; return tagged;
} }
@ -184,13 +192,53 @@ public final class EventStripe implements EventBundle<EventCluster> {
} }
@Override @Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ImmutableSortedSet< EventCluster> getClusters() { public ImmutableSortedSet< EventCluster> getClusters() {
return clusters; return clusters;
} }
@Override @Override
public String toString() { public String toString() {
return "EventStripe{" + "description=" + description + ", eventIDs=" + eventIDs.size() + '}'; //NON-NLS return "EventStripe{" + "description=" + description + ", eventIDs=" + (Objects.isNull(eventIDs) ? 0 : eventIDs.size()) + '}'; //NON-NLS
}
@Override
public int hashCode() {
int hash = 3;
hash = 79 * hash + Objects.hashCode(this.clusters);
hash = 79 * hash + Objects.hashCode(this.type);
hash = 79 * hash + Objects.hashCode(this.description);
hash = 79 * hash + Objects.hashCode(this.lod);
hash = 79 * hash + Objects.hashCode(this.eventIDs);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final EventStripe other = (EventStripe) obj;
if (!Objects.equals(this.description, other.description)) {
return false;
}
if (!Objects.equals(this.clusters, other.clusters)) {
return false;
}
if (!Objects.equals(this.type, other.type)) {
return false;
}
if (this.lod != other.lod) {
return false;
}
if (!Objects.equals(this.eventIDs, other.eventIDs)) {
return false;
}
return true;
} }
} }

View File

@ -44,7 +44,8 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType;
import org.sleuthkit.autopsy.timeline.db.EventsRepository; import org.sleuthkit.autopsy.timeline.db.EventsRepository;
import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent;
import org.sleuthkit.autopsy.timeline.events.TagsUpdatedEvent; import org.sleuthkit.autopsy.timeline.events.TagsAddedEvent;
import org.sleuthkit.autopsy.timeline.events.TagsDeletedEvent;
import org.sleuthkit.autopsy.timeline.filters.DataSourceFilter; import org.sleuthkit.autopsy.timeline.filters.DataSourceFilter;
import org.sleuthkit.autopsy.timeline.filters.DataSourcesFilter; import org.sleuthkit.autopsy.timeline.filters.DataSourcesFilter;
import org.sleuthkit.autopsy.timeline.filters.Filter; import org.sleuthkit.autopsy.timeline.filters.Filter;
@ -232,11 +233,11 @@ public final class FilteredEventsModel {
return repo.getBoundingEventsInterval(zoomParametersProperty().get().getTimeRange(), zoomParametersProperty().get().getFilter()); return repo.getBoundingEventsInterval(zoomParametersProperty().get().getTimeRange(), zoomParametersProperty().get().getFilter());
} }
public TimeLineEvent getEventById(Long eventID) { public SingleEvent getEventById(Long eventID) {
return repo.getEventById(eventID); return repo.getEventById(eventID);
} }
public Set<TimeLineEvent> getEventsById(Collection<Long> eventIDs) { public Set<SingleEvent> getEventsById(Collection<Long> eventIDs) {
return repo.getEventsById(eventIDs); return repo.getEventsById(eventIDs);
} }
@ -351,14 +352,14 @@ public final class FilteredEventsModel {
ContentTag contentTag = evt.getAddedTag(); ContentTag contentTag = evt.getAddedTag();
Content content = contentTag.getContent(); Content content = contentTag.getContent();
Set<Long> updatedEventIDs = repo.addTag(content.getId(), null, contentTag, null); Set<Long> updatedEventIDs = repo.addTag(content.getId(), null, contentTag, null);
return postTagsUpdated(updatedEventIDs); return postTagsAdded(updatedEventIDs);
} }
synchronized public boolean handleArtifactTagAdded(BlackBoardArtifactTagAddedEvent evt) { synchronized public boolean handleArtifactTagAdded(BlackBoardArtifactTagAddedEvent evt) {
BlackboardArtifactTag artifactTag = evt.getAddedTag(); BlackboardArtifactTag artifactTag = evt.getAddedTag();
BlackboardArtifact artifact = artifactTag.getArtifact(); BlackboardArtifact artifact = artifactTag.getArtifact();
Set<Long> updatedEventIDs = repo.addTag(artifact.getObjectID(), artifact.getArtifactID(), artifactTag, null); Set<Long> updatedEventIDs = repo.addTag(artifact.getObjectID(), artifact.getArtifactID(), artifactTag, null);
return postTagsUpdated(updatedEventIDs); return postTagsAdded(updatedEventIDs);
} }
synchronized public boolean handleContentTagDeleted(ContentTagDeletedEvent evt) { synchronized public boolean handleContentTagDeleted(ContentTagDeletedEvent evt) {
@ -367,7 +368,7 @@ public final class FilteredEventsModel {
Content content = autoCase.getSleuthkitCase().getContentById(deletedTagInfo.getContentID()); Content content = autoCase.getSleuthkitCase().getContentById(deletedTagInfo.getContentID());
boolean tagged = autoCase.getServices().getTagsManager().getContentTagsByContent(content).isEmpty() == false; boolean tagged = autoCase.getServices().getTagsManager().getContentTagsByContent(content).isEmpty() == false;
Set<Long> updatedEventIDs = repo.deleteTag(content.getId(), null, deletedTagInfo.getTagID(), tagged); Set<Long> updatedEventIDs = repo.deleteTag(content.getId(), null, deletedTagInfo.getTagID(), tagged);
return postTagsUpdated(updatedEventIDs); return postTagsDeleted(updatedEventIDs);
} catch (TskCoreException ex) { } catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "unable to determine tagged status of content.", ex); //NON-NLS LOGGER.log(Level.SEVERE, "unable to determine tagged status of content.", ex); //NON-NLS
} }
@ -380,17 +381,25 @@ public final class FilteredEventsModel {
BlackboardArtifact artifact = autoCase.getSleuthkitCase().getBlackboardArtifact(deletedTagInfo.getArtifactID()); BlackboardArtifact artifact = autoCase.getSleuthkitCase().getBlackboardArtifact(deletedTagInfo.getArtifactID());
boolean tagged = autoCase.getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact).isEmpty() == false; boolean tagged = autoCase.getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact).isEmpty() == false;
Set<Long> updatedEventIDs = repo.deleteTag(artifact.getObjectID(), artifact.getArtifactID(), deletedTagInfo.getTagID(), tagged); Set<Long> updatedEventIDs = repo.deleteTag(artifact.getObjectID(), artifact.getArtifactID(), deletedTagInfo.getTagID(), tagged);
return postTagsUpdated(updatedEventIDs); return postTagsDeleted(updatedEventIDs);
} catch (TskCoreException ex) { } catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "unable to determine tagged status of artifact.", ex); //NON-NLS LOGGER.log(Level.SEVERE, "unable to determine tagged status of artifact.", ex); //NON-NLS
} }
return false; return false;
} }
private boolean postTagsUpdated(Set<Long> updatedEventIDs) { private boolean postTagsAdded(Set<Long> updatedEventIDs) {
boolean tagsUpdated = !updatedEventIDs.isEmpty(); boolean tagsUpdated = !updatedEventIDs.isEmpty();
if (tagsUpdated) { if (tagsUpdated) {
eventbus.post(new TagsUpdatedEvent(updatedEventIDs)); eventbus.post(new TagsAddedEvent(updatedEventIDs));
}
return tagsUpdated;
}
private boolean postTagsDeleted(Set<Long> updatedEventIDs) {
boolean tagsUpdated = !updatedEventIDs.isEmpty();
if (tagsUpdated) {
eventbus.post(new TagsDeletedEvent(updatedEventIDs));
} }
return tagsUpdated; return tagsUpdated;
} }

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");
@ -19,37 +19,16 @@
package org.sleuthkit.autopsy.timeline.datamodel; package org.sleuthkit.autopsy.timeline.datamodel;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
/** /**
* A interface for groups of events that share some attributes in common. * A interface for groups of events that share some attributes in common.
*/ */
public interface EventBundle<ParentType extends EventBundle<?>> { public interface MultiEvent<ParentType extends MultiEvent<?>> extends TimeLineEvent {
String getDescription();
DescriptionLoD getDescriptionLoD();
Set<Long> getEventIDs();
Set<Long> getEventIDsWithHashHits();
Set<Long> getEventIDsWithTags();
EventType getEventType();
long getEndMillis(); long getEndMillis();
long getStartMillis(); Optional<ParentType> getParent();
Optional<ParentType> getParentBundle();
default int getCount() {
return getEventIDs().size();
}
SortedSet<EventCluster> getClusters(); SortedSet<EventCluster> getClusters();
} }

View File

@ -0,0 +1,187 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2014-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.timeline.datamodel;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.Set;
import java.util.SortedSet;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.joda.time.Interval;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
import org.sleuthkit.datamodel.TskData;
/**
* A single event.
*/
@Immutable
public class SingleEvent implements TimeLineEvent {
private final long eventID;
private final long fileID;
private final Long artifactID;
private final long dataSourceID;
private final long time;
private final EventType subType;
private final ImmutableMap<DescriptionLoD, String> descriptions;
private final TskData.FileKnown known;
private final boolean hashHit;
private final boolean tagged;
public SingleEvent(long eventID, long dataSourceID, long objID, @Nullable Long artifactID, long time, EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, boolean hashHit, boolean tagged) {
this.eventID = eventID;
this.fileID = objID;
this.artifactID = artifactID == 0 ? null : artifactID;
this.time = time;
this.subType = type;
descriptions = ImmutableMap.<DescriptionLoD, String>of(DescriptionLoD.FULL, fullDescription,
DescriptionLoD.MEDIUM, medDescription,
DescriptionLoD.SHORT, shortDescription);
this.known = known;
this.hashHit = hashHit;
this.tagged = tagged;
this.dataSourceID = dataSourceID;
}
public boolean isTagged() {
return tagged;
}
public boolean isHashHit() {
return hashHit;
}
@Nullable
public Long getArtifactID() {
return artifactID;
}
public long getEventID() {
return eventID;
}
public long getFileID() {
return fileID;
}
/**
* @return the time in seconds from unix epoch
*/
public long getTime() {
return time;
}
public EventType getEventType() {
return subType;
}
public String getFullDescription() {
return getDescription(DescriptionLoD.FULL);
}
public String getMedDescription() {
return getDescription(DescriptionLoD.MEDIUM);
}
public String getShortDescription() {
return getDescription(DescriptionLoD.SHORT);
}
public TskData.FileKnown getKnown() {
return known;
}
public String getDescription(DescriptionLoD lod) {
return descriptions.get(lod);
}
public long getDataSourceID() {
return dataSourceID;
}
@Override
public Set<Long> getEventIDs() {
return Collections.singleton(eventID);
}
@Override
public Set<Long> getEventIDsWithHashHits() {
return isHashHit() ? Collections.singleton(eventID) : Collections.emptySet();
}
@Override
public Set<Long> getEventIDsWithTags() {
return isTagged() ? Collections.singleton(eventID) : Collections.emptySet();
}
@Override
public long getEndMillis() {
return time * 1000;
}
@Override
public long getStartMillis() {
return time * 1000;
}
@Override
public int hashCode() {
int hash = 7;
hash = 13 * hash + (int) (this.eventID ^ (this.eventID >>> 32));
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final SingleEvent other = (SingleEvent) obj;
if (this.eventID != other.eventID) {
return false;
}
return true;
}
@Override
public SortedSet<EventCluster> getClusters() {
EventCluster eventCluster = new EventCluster(new Interval(time * 1000, time * 1000), subType, getEventIDs(), getEventIDsWithHashHits(), getEventIDsWithTags(), getFullDescription(), DescriptionLoD.FULL);
return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(eventCluster).build();
}
@Override
public String getDescription() {
return getFullDescription();
}
@Override
public DescriptionLoD getDescriptionLoD() {
return DescriptionLoD.FULL;
}
}

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2014-15 Basis Technology Corp. * Copyright 2016 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,101 +18,35 @@
*/ */
package org.sleuthkit.autopsy.timeline.datamodel; package org.sleuthkit.autopsy.timeline.datamodel;
import com.google.common.collect.ImmutableMap; import java.util.Set;
import javax.annotation.Nullable; import java.util.SortedSet;
import javax.annotation.concurrent.Immutable;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
import org.sleuthkit.datamodel.TskData;
/** /**
* A single event. *
*/ */
@Immutable public interface TimeLineEvent {
public class TimeLineEvent {
private final long eventID; public String getDescription();
private final long fileID;
private final Long artifactID;
private final long dataSourceID;
private final long time; public DescriptionLoD getDescriptionLoD();
private final EventType subType;
private final ImmutableMap<DescriptionLoD, String> descriptions;
private final TskData.FileKnown known; Set<Long> getEventIDs();
private final boolean hashHit;
private final boolean tagged;
public TimeLineEvent(long eventID, long dataSourceID, long objID, @Nullable Long artifactID, long time, EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, boolean hashHit, boolean tagged) { Set<Long> getEventIDsWithHashHits();
this.eventID = eventID;
this.fileID = objID;
this.artifactID = artifactID == 0 ? null : artifactID;
this.time = time;
this.subType = type;
descriptions = ImmutableMap.<DescriptionLoD, String>of(DescriptionLoD.FULL, fullDescription,
DescriptionLoD.MEDIUM, medDescription,
DescriptionLoD.SHORT, shortDescription);
this.known = known; Set<Long> getEventIDsWithTags();
this.hashHit = hashHit;
this.tagged = tagged; EventType getEventType();
this.dataSourceID = dataSourceID;
long getEndMillis();
long getStartMillis();
default int getSize() {
return getEventIDs().size();
} }
public boolean isTagged() { SortedSet<EventCluster> getClusters();
return tagged;
}
public boolean isHashHit() {
return hashHit;
}
@Nullable
public Long getArtifactID() {
return artifactID;
}
public long getEventID() {
return eventID;
}
public long getFileID() {
return fileID;
}
/**
* @return the time in seconds from unix epoch
*/
public long getTime() {
return time;
}
public EventType getType() {
return subType;
}
public String getFullDescription() {
return getDescription(DescriptionLoD.FULL);
}
public String getMedDescription() {
return getDescription(DescriptionLoD.MEDIUM);
}
public String getShortDescription() {
return getDescription(DescriptionLoD.SHORT);
}
public TskData.FileKnown getKnown() {
return known;
}
public String getDescription(DescriptionLoD lod) {
return descriptions.get(lod);
}
public long getDataSourceID() {
return dataSourceID;
}
} }

View File

@ -58,7 +58,7 @@ import org.sleuthkit.autopsy.coreutils.Version;
import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.BaseTypes; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.BaseTypes;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType;
@ -323,8 +323,8 @@ public class EventDB {
return null; return null;
} }
TimeLineEvent getEventById(Long eventID) { SingleEvent getEventById(Long eventID) {
TimeLineEvent result = null; SingleEvent result = null;
DBLock.lock(); DBLock.lock();
try { try {
getEventByIDStmt.clearParameters(); getEventByIDStmt.clearParameters();
@ -939,8 +939,8 @@ public class EventDB {
} }
} }
private TimeLineEvent constructTimeLineEvent(ResultSet rs) throws SQLException { private SingleEvent constructTimeLineEvent(ResultSet rs) throws SQLException {
return new TimeLineEvent(rs.getLong("event_id"), //NON-NLS return new SingleEvent(rs.getLong("event_id"), //NON-NLS
rs.getLong("datasource_id"), //NON-NLS rs.getLong("datasource_id"), //NON-NLS
rs.getLong("file_id"), //NON-NLS rs.getLong("file_id"), //NON-NLS
rs.getLong("artifact_id"), //NON-NLS rs.getLong("artifact_id"), //NON-NLS
@ -1050,7 +1050,7 @@ public class EventDB {
switch (Version.getBuildType()) { switch (Version.getBuildType()) {
case DEVELOPMENT: case DEVELOPMENT:
LOGGER.log(Level.INFO, "executing timeline query: {0}", query); //NON-NLS // LOGGER.log(Level.INFO, "executing timeline query: {0}", query); //NON-NLS
break; break;
case RELEASE: case RELEASE:
default: default:
@ -1097,8 +1097,7 @@ public class EventDB {
Set<Long> hashHits = SQLHelper.unGroupConcat(rs.getString("hash_hits"), Long::valueOf); //NON-NLS Set<Long> hashHits = SQLHelper.unGroupConcat(rs.getString("hash_hits"), Long::valueOf); //NON-NLS
Set<Long> tagged = SQLHelper.unGroupConcat(rs.getString("taggeds"), Long::valueOf); //NON-NLS Set<Long> tagged = SQLHelper.unGroupConcat(rs.getString("taggeds"), Long::valueOf); //NON-NLS
return new EventCluster(interval, type, eventIDs, hashHits, tagged, return new EventCluster(interval, type, eventIDs, hashHits, tagged, description, descriptionLOD);
description, descriptionLOD);
} }
/** /**
@ -1159,7 +1158,7 @@ public class EventDB {
for (EventCluster eventCluster : aggEvents) { for (EventCluster eventCluster : aggEvents) {
stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()), stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()),
new EventStripe(eventCluster, null), EventStripe::merge); new EventStripe(eventCluster), EventStripe::merge);
} }
return stripeDescMap.values().stream().sorted(Comparator.comparing(EventStripe::getStartMillis)).collect(Collectors.toList()); return stripeDescMap.values().stream().sorted(Comparator.comparing(EventStripe::getStartMillis)).collect(Collectors.toList());

View File

@ -59,7 +59,7 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.CancellationProgressTask; import org.sleuthkit.autopsy.timeline.CancellationProgressTask;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.ArtifactEventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.ArtifactEventType;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.FileSystemTypes; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.FileSystemTypes;
@ -105,7 +105,7 @@ public class EventsRepository {
private final LoadingCache<Object, Long> maxCache; private final LoadingCache<Object, Long> maxCache;
private final LoadingCache<Object, Long> minCache; private final LoadingCache<Object, Long> minCache;
private final LoadingCache<Long, TimeLineEvent> idToEventCache; private final LoadingCache<Long, SingleEvent> idToEventCache;
private final LoadingCache<ZoomParams, Map<EventType, Long>> eventCountsCache; private final LoadingCache<ZoomParams, Map<EventType, Long>> eventCountsCache;
private final LoadingCache<ZoomParams, List<EventStripe>> eventStripeCache; private final LoadingCache<ZoomParams, List<EventStripe>> eventStripeCache;
@ -179,11 +179,11 @@ public class EventsRepository {
} }
public TimeLineEvent getEventById(Long eventID) { public SingleEvent getEventById(Long eventID) {
return idToEventCache.getUnchecked(eventID); return idToEventCache.getUnchecked(eventID);
} }
synchronized public Set<TimeLineEvent> getEventsById(Collection<Long> eventIDs) { synchronized public Set<SingleEvent> getEventsById(Collection<Long> eventIDs) {
return eventIDs.stream() return eventIDs.stream()
.map(idToEventCache::getUnchecked) .map(idToEventCache::getUnchecked)
.collect(Collectors.toSet()); .collect(Collectors.toSet());

View File

@ -0,0 +1,15 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.sleuthkit.autopsy.timeline.events;
import java.util.Set;
public class TagsAddedEvent extends TagsUpdatedEvent {
public TagsAddedEvent(Set<Long> updatedEventIDs) {
super(updatedEventIDs);
}
}

View File

@ -0,0 +1,17 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.sleuthkit.autopsy.timeline.events;
import java.util.Set;
public class TagsDeletedEvent extends TagsUpdatedEvent {
public TagsDeletedEvent(Set<Long> updatedEventIDs) {
super(updatedEventIDs);
}
}

View File

@ -26,16 +26,14 @@ import java.util.Set;
* been(un)tagged. This event is not intended for use out side of the timeline * been(un)tagged. This event is not intended for use out side of the timeline
* module. * module.
*/ */
public class TagsUpdatedEvent { abstract public class TagsUpdatedEvent {
private final Set<Long> updatedEventIDs; private final Set<Long> updatedEventIDs;
public ImmutableSet<Long> getUpdatedEventIDs() { public ImmutableSet<Long> getUpdatedEventIDs() {
return ImmutableSet.copyOf(updatedEventIDs); return ImmutableSet.copyOf(updatedEventIDs);
} }
public TagsUpdatedEvent(Set<Long> updatedEventIDs) { public TagsUpdatedEvent(Set<Long> updatedEventIDs) {
this.updatedEventIDs = updatedEventIDs; this.updatedEventIDs = updatedEventIDs;

View File

@ -37,30 +37,30 @@ import org.sleuthkit.autopsy.datamodel.DisplayableItemNode;
import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor;
import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.datamodel.NodeProperty;
import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Content;
/** /**
* * Explorer Node for {@link TimeLineEvent}s. * * Explorer Node for {@link SingleEvent}s.
*/ */
class EventNode extends DisplayableItemNode { class EventNode extends DisplayableItemNode {
private static final Logger LOGGER = Logger.getLogger(EventNode.class.getName()); private static final Logger LOGGER = Logger.getLogger(EventNode.class.getName());
private final TimeLineEvent e; private final SingleEvent e;
EventNode(TimeLineEvent eventById, AbstractFile file, BlackboardArtifact artifact) { EventNode(SingleEvent eventById, AbstractFile file, BlackboardArtifact artifact) {
super(Children.LEAF, Lookups.fixed(eventById, file, artifact)); super(Children.LEAF, Lookups.fixed(eventById, file, artifact));
this.e = eventById; this.e = eventById;
this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + e.getType().getIconBase()); // NON-NLS this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + e.getEventType().getIconBase()); // NON-NLS
} }
EventNode(TimeLineEvent eventById, AbstractFile file) { EventNode(SingleEvent eventById, AbstractFile file) {
super(Children.LEAF, Lookups.fixed(eventById, file)); super(Children.LEAF, Lookups.fixed(eventById, file));
this.e = eventById; this.e = eventById;
this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + e.getType().getIconBase()); // NON-NLS this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + e.getEventType().getIconBase()); // NON-NLS
} }
@Override @Override
@ -85,15 +85,15 @@ class EventNode extends DisplayableItemNode {
properties.put(new NodeProperty<>("icon", "Icon", "icon", true)); // NON-NLS //gets overridden with icon properties.put(new NodeProperty<>("icon", "Icon", "icon", true)); // NON-NLS //gets overridden with icon
properties.put(timePropery); properties.put(timePropery);
properties.put(new NodeProperty<>("description", "Description", "description", e.getFullDescription())); // NON-NLS properties.put(new NodeProperty<>("description", "Description", "description", e.getFullDescription())); // NON-NLS
properties.put(new NodeProperty<>("eventBaseType", "Base Type", "base type", e.getType().getSuperType().getDisplayName())); // NON-NLS properties.put(new NodeProperty<>("eventBaseType", "Base Type", "base type", e.getEventType().getSuperType().getDisplayName())); // NON-NLS
properties.put(new NodeProperty<>("eventSubType", "Sub Type", "sub type", e.getType().getDisplayName())); // NON-NLS properties.put(new NodeProperty<>("eventSubType", "Sub Type", "sub type", e.getEventType().getDisplayName())); // NON-NLS
properties.put(new NodeProperty<>("Known", "Known", "known", e.getKnown().toString())); // NON-NLS properties.put(new NodeProperty<>("Known", "Known", "known", e.getKnown().toString())); // NON-NLS
return s; return s;
} }
private String getDateTimeString() { private String getDateTimeString() {
return new DateTime(e.getTime() * 1000, DateTimeZone.UTC).toString(TimeLineController.getZonedFormatter()); return new DateTime(e.getStartMillis(), DateTimeZone.UTC).toString(TimeLineController.getZonedFormatter());
} }
@Override @Override

View File

@ -32,7 +32,7 @@ import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode;
import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.BaseTypes; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.BaseTypes;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
@ -111,11 +111,11 @@ public class EventRootNode extends DisplayableItemNode {
@Override @Override
protected Node createNodeForKey(Long eventID) { protected Node createNodeForKey(Long eventID) {
if (eventID >= 0) { if (eventID >= 0) {
final TimeLineEvent eventById = filteredEvents.getEventById(eventID); final SingleEvent eventById = filteredEvents.getEventById(eventID);
try { try {
AbstractFile file = Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(eventById.getFileID()); AbstractFile file = Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(eventById.getFileID());
if (file != null) { if (file != null) {
if (eventById.getType().getSuperType() == BaseTypes.FILE_SYSTEM) { if (eventById.getEventType().getSuperType() == BaseTypes.FILE_SYSTEM) {
return new EventNode(eventById, file); return new EventNode(eventById, file);
} else { } else {
BlackboardArtifact blackboardArtifact = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifact(eventById.getArtifactID()); BlackboardArtifact blackboardArtifact = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifact(eventById.getArtifactID());

View File

@ -60,7 +60,7 @@ public abstract class AbstractFilter implements Filter {
@Override @Override
public boolean isDisabled() { public boolean isDisabled() {
return disabled.get(); return disabledProperty().get();
} }
@Override @Override

View File

@ -18,7 +18,7 @@
*/ */
package org.sleuthkit.autopsy.timeline.filters; package org.sleuthkit.autopsy.timeline.filters;
import java.util.Comparator; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableBooleanValue;
@ -39,9 +39,9 @@ public class DataSourcesFilter extends UnionFilter<DataSourceFilter> {
filterCopy.setSelected(isSelected()); filterCopy.setSelected(isSelected());
filterCopy.setDisabled(isDisabled()); filterCopy.setDisabled(isDisabled());
//add a copy of each subfilter //add a copy of each subfilter
this.getSubFilters().forEach((DataSourceFilter t) -> { getSubFilters().forEach(dataSourceFilter ->
filterCopy.addSubFilter(t.copyOf()); filterCopy.addSubFilter(dataSourceFilter.copyOf())
}); );
return filterCopy; return filterCopy;
} }
@ -66,13 +66,7 @@ public class DataSourcesFilter extends UnionFilter<DataSourceFilter> {
} }
public void addSubFilter(DataSourceFilter dataSourceFilter) { public void addSubFilter(DataSourceFilter dataSourceFilter) {
if (getSubFilters().stream().map(DataSourceFilter.class::cast) super.addSubFilter(dataSourceFilter);
.map(DataSourceFilter::getDataSourceID)
.filter(t -> t == dataSourceFilter.getDataSourceID())
.findAny().isPresent() == false) {
getSubFilters().add(dataSourceFilter);
getSubFilters().sort(Comparator.comparing(DataSourceFilter::getDisplayName));
}
if (getSubFilters().size() > 1) { if (getSubFilters().size() > 1) {
setSelected(Boolean.TRUE); setSelected(Boolean.TRUE);
} }
@ -106,4 +100,8 @@ public class DataSourcesFilter extends UnionFilter<DataSourceFilter> {
return Bindings.or(super.disabledProperty(), Bindings.size(getSubFilters()).lessThanOrEqualTo(1)); return Bindings.or(super.disabledProperty(), Bindings.size(getSubFilters()).lessThanOrEqualTo(1));
} }
@Override
Predicate<DataSourceFilter> getDuplicatePredicate(DataSourceFilter subfilter) {
return dataSourcefilter -> dataSourcefilter.getDataSourceID() == subfilter.getDataSourceID();
}
} }

View File

@ -18,7 +18,7 @@
*/ */
package org.sleuthkit.autopsy.timeline.filters; package org.sleuthkit.autopsy.timeline.filters;
import java.util.Comparator; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableBooleanValue;
@ -45,9 +45,9 @@ public class HashHitsFilter extends UnionFilter<HashSetFilter> {
filterCopy.setSelected(isSelected()); filterCopy.setSelected(isSelected());
filterCopy.setDisabled(isDisabled()); filterCopy.setDisabled(isDisabled());
//add a copy of each subfilter //add a copy of each subfilter
this.getSubFilters().forEach((HashSetFilter t) -> { this.getSubFilters().forEach(hashSetFilter ->
filterCopy.addSubFilter(t.copyOf()); filterCopy.addSubFilter(hashSetFilter.copyOf())
}); );
return filterCopy; return filterCopy;
} }
@ -86,18 +86,13 @@ public class HashHitsFilter extends UnionFilter<HashSetFilter> {
return areSubFiltersEqual(this, other); return areSubFiltersEqual(this, other);
} }
public void addSubFilter(HashSetFilter hashSetFilter) {
if (getSubFilters().stream()
.map(HashSetFilter::getHashSetID)
.filter(t -> t == hashSetFilter.getHashSetID())
.findAny().isPresent() == false) {
getSubFilters().add(hashSetFilter);
getSubFilters().sort(Comparator.comparing(HashSetFilter::getDisplayName));
}
}
@Override @Override
public ObservableBooleanValue disabledProperty() { public ObservableBooleanValue disabledProperty() {
return Bindings.or(super.disabledProperty(), Bindings.isEmpty(getSubFilters())); return Bindings.or(super.disabledProperty(), Bindings.isEmpty(getSubFilters()));
} }
@Override
Predicate<HashSetFilter> getDuplicatePredicate(HashSetFilter subfilter) {
return hashSetFilter -> subfilter.getHashSetID() == hashSetFilter.getHashSetID();
}
} }

View File

@ -84,5 +84,4 @@ public class HashSetFilter extends AbstractFilter {
} }
return isSelected() == other.isSelected(); return isSelected() == other.isSelected();
} }
} }

View File

@ -19,6 +19,7 @@
package org.sleuthkit.autopsy.timeline.filters; package org.sleuthkit.autopsy.timeline.filters;
import java.util.Comparator; import java.util.Comparator;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableBooleanValue;
@ -46,9 +47,9 @@ public class TagsFilter extends UnionFilter<TagNameFilter> {
filterCopy.setSelected(isSelected()); filterCopy.setSelected(isSelected());
filterCopy.setDisabled(isDisabled()); filterCopy.setDisabled(isDisabled());
//add a copy of each subfilter //add a copy of each subfilter
this.getSubFilters().forEach((TagNameFilter t) -> { getSubFilters().forEach(tagNameFilter ->
filterCopy.addSubFilter(t.copyOf()); filterCopy.addSubFilter(tagNameFilter.copyOf())
}); );
return filterCopy; return filterCopy;
} }
@ -87,17 +88,6 @@ public class TagsFilter extends UnionFilter<TagNameFilter> {
return areSubFiltersEqual(this, other); return areSubFiltersEqual(this, other);
} }
public void addSubFilter(TagNameFilter tagFilter) {
TagName newFilterTagName = tagFilter.getTagName();
if (getSubFilters().stream()
.map(TagNameFilter::getTagName)
.filter(newFilterTagName::equals)
.findAny().isPresent() == false) {
getSubFilters().add(tagFilter);
}
getSubFilters().sort(Comparator.comparing(TagNameFilter::getDisplayName));
}
public void removeFilterForTag(TagName tagName) { public void removeFilterForTag(TagName tagName) {
getSubFilters().removeIf(subfilter -> subfilter.getTagName().equals(tagName)); getSubFilters().removeIf(subfilter -> subfilter.getTagName().equals(tagName));
getSubFilters().sort(Comparator.comparing(TagNameFilter::getDisplayName)); getSubFilters().sort(Comparator.comparing(TagNameFilter::getDisplayName));
@ -107,4 +97,10 @@ public class TagsFilter extends UnionFilter<TagNameFilter> {
public ObservableBooleanValue disabledProperty() { public ObservableBooleanValue disabledProperty() {
return Bindings.or(super.disabledProperty(), Bindings.isEmpty(getSubFilters())); return Bindings.or(super.disabledProperty(), Bindings.isEmpty(getSubFilters()));
} }
@Override
Predicate<TagNameFilter> getDuplicatePredicate(TagNameFilter subfilter) {
return tagNameFilter -> subfilter.getTagName().equals(tagNameFilter.getTagName());
}
} }

View File

@ -18,7 +18,9 @@
*/ */
package org.sleuthkit.autopsy.timeline.filters; package org.sleuthkit.autopsy.timeline.filters;
import java.util.Comparator;
import java.util.Objects; import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.scene.image.Image; import javafx.scene.image.Image;
@ -33,6 +35,8 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType;
*/ */
public class TypeFilter extends UnionFilter<TypeFilter> { public class TypeFilter extends UnionFilter<TypeFilter> {
static private final Comparator<TypeFilter> comparator = Comparator.comparing(TypeFilter::getEventType, EventType.getComparator());
/** /**
* the event type this filter passes * the event type this filter passes
*/ */
@ -52,7 +56,7 @@ public class TypeFilter extends UnionFilter<TypeFilter> {
if (recursive) { // add subfilters for each subtype if (recursive) { // add subfilters for each subtype
for (EventType subType : et.getSubTypes()) { for (EventType subType : et.getSubTypes()) {
this.getSubFilters().add(new TypeFilter(subType)); addSubFilter(new TypeFilter(subType), comparator);
} }
} }
} }
@ -96,15 +100,15 @@ public class TypeFilter extends UnionFilter<TypeFilter> {
@Override @Override
public TypeFilter copyOf() { public TypeFilter copyOf() {
//make a nonrecursive copy of this filter //make a nonrecursive copy of this filter
final TypeFilter typeFilter = new TypeFilter(eventType, false); final TypeFilter filterCopy = new TypeFilter(eventType, false);
typeFilter.setSelected(isSelected()); filterCopy.setSelected(isSelected());
typeFilter.setDisabled(isDisabled()); filterCopy.setDisabled(isDisabled());
//add a copy of each subfilter //add a copy of each subfilter
this.getSubFilters().forEach((TypeFilter t) -> { getSubFilters().forEach(typeFilter ->
typeFilter.getSubFilters().add(t.copyOf()); filterCopy.addSubFilter(typeFilter.copyOf(), comparator)
}); );
return typeFilter; return filterCopy;
} }
@Override @Override
@ -142,4 +146,9 @@ public class TypeFilter extends UnionFilter<TypeFilter> {
hash = 67 * hash + Objects.hashCode(this.eventType); hash = 67 * hash + Objects.hashCode(this.eventType);
return hash; return hash;
} }
@Override
Predicate<TypeFilter> getDuplicatePredicate(TypeFilter subfilter) {
return t -> subfilter.getEventType().equals(t.eventType);
}
} }

View File

@ -18,6 +18,8 @@
*/ */
package org.sleuthkit.autopsy.timeline.filters; package org.sleuthkit.autopsy.timeline.filters;
import java.util.Comparator;
import java.util.function.Predicate;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -34,5 +36,17 @@ abstract public class UnionFilter<SubFilterType extends Filter> extends Compound
super(FXCollections.<SubFilterType>observableArrayList()); super(FXCollections.<SubFilterType>observableArrayList());
} }
abstract Predicate<SubFilterType> getDuplicatePredicate(SubFilterType subfilter);
public void addSubFilter(SubFilterType subfilter) {
addSubFilter(subfilter, Comparator.comparing(SubFilterType::getDisplayName));
}
protected void addSubFilter(SubFilterType subfilter, Comparator<SubFilterType> comparator) {
Predicate<SubFilterType> duplicatePredicate = getDuplicatePredicate(subfilter);
if (getSubFilters().stream().anyMatch(duplicatePredicate) == false) {
getSubFilters().add(subfilter);
}
getSubFilters().sort(comparator);
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

View File

@ -45,8 +45,6 @@ import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle; import javafx.scene.control.OverrunStyle;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.scene.effect.Effect; import javafx.scene.effect.Effect;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
@ -82,7 +80,7 @@ import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent;
* {@link XYChart} doing the rendering. Is this a good idea? -jm TODO: pull up * {@link XYChart} doing the rendering. Is this a good idea? -jm TODO: pull up
* common history context menu items out of derived classes? -jm * common history context menu items out of derived classes? -jm
*/ */
public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, ChartType extends XYChart<X, Y> & TimeLineChart<X>> extends BorderPane { public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, ChartType extends Region & TimeLineChart<X>> extends BorderPane {
@NbBundle.Messages("AbstractVisualization.Default_Tooltip.text=Drag the mouse to select a time interval to zoom into.\nRight-click for more actions.") @NbBundle.Messages("AbstractVisualization.Default_Tooltip.text=Drag the mouse to select a time interval to zoom into.\nRight-click for more actions.")
private static final Tooltip DEFAULT_TOOLTIP = new Tooltip(Bundle.AbstractVisualization_Default_Tooltip_text()); private static final Tooltip DEFAULT_TOOLTIP = new Tooltip(Bundle.AbstractVisualization_Default_Tooltip_text());
@ -132,6 +130,10 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
*/ */
protected List<Node> settingsNodes; protected List<Node> settingsNodes;
public TimeLineController getController() {
return controller;
}
/** /**
* @return the list of nodes containing settings widgets to insert into this * @return the list of nodes containing settings widgets to insert into this
* visualization's header * visualization's header
@ -195,6 +197,7 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
*/ */
abstract protected Axis<Y> getYAxis(); abstract protected Axis<Y> getYAxis();
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
abstract protected void resetData(); abstract protected void resetData();
/** /**
@ -378,14 +381,6 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
requestParentLayout(); requestParentLayout();
} }
protected void setChartClickHandler() {
chart.addEventHandler(MouseEvent.MOUSE_CLICKED, (MouseEvent event) -> {
if (event.getButton() == MouseButton.PRIMARY && event.isStillSincePress()) {
selectedNodes.clear();
}
});
}
/** /**
* add a {@link Text} node to the leaf container for the decluttered axis * add a {@link Text} node to the leaf container for the decluttered axis
* labels * labels
@ -512,12 +507,13 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
MaskerPane maskerPane = new MaskerPane(); MaskerPane maskerPane = new MaskerPane();
maskerPane.textProperty().bind(messageProperty()); maskerPane.textProperty().bind(messageProperty());
maskerPane.progressProperty().bind(progressProperty()); maskerPane.progressProperty().bind(progressProperty());
setCenter(new StackPane(chart, maskerPane)); setCenter(new StackPane(center, maskerPane));
setCursor(Cursor.WAIT); setCursor(Cursor.WAIT);
}); });
return true; return true;
} }
private final Node center = getCenter();
/** /**
* updates the horisontal axis and removes the blocking progress * updates the horisontal axis and removes the blocking progress
@ -530,7 +526,7 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
layoutDateLabels(); layoutDateLabels();
Platform.runLater(() -> { Platform.runLater(() -> {
setCenter(chart); //clear masker pane setCenter(center); //clear masker pane
setCursor(Cursor.DEFAULT); setCursor(Cursor.DEFAULT);
}); });
} }
@ -543,11 +539,11 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
*/ */
@ThreadConfined(type = ThreadConfined.ThreadType.NOT_UI) @ThreadConfined(type = ThreadConfined.ThreadType.NOT_UI)
protected void resetChart(AxisValuesType axisValues) { protected void resetChart(AxisValuesType axisValues) {
resetData();
Platform.runLater(() -> { Platform.runLater(() -> {
resetData();
setDateAxisValues(axisValues); setDateAxisValues(axisValues);
}); });
} }
abstract protected void setDateAxisValues(AxisValuesType values); abstract protected void setDateAxisValues(AxisValuesType values);

View File

@ -0,0 +1,19 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.sleuthkit.autopsy.timeline.ui;
import javafx.scene.control.ContextMenu;
import javafx.scene.input.MouseEvent;
import org.sleuthkit.autopsy.timeline.TimeLineController;
public interface ContextMenuProvider {
TimeLineController getController();
void clearContextMenu();
ContextMenu getContextMenu(MouseEvent m);
}

View File

@ -1,12 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?> <?import javafx.geometry.Insets?>
<?import javafx.scene.image.*?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.Label?>
<?import java.lang.*?> <?import javafx.scene.image.Image?>
<?import javafx.scene.layout.*?> <?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<fx:root maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" styleClass="intervalSelector" stylesheets="@IntervalSelector.css" type="BorderPane" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1"> <fx:root maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" pickOnBounds="false" prefHeight="400.0" prefWidth="600.0" styleClass="intervalSelector" stylesheets="@IntervalSelector.css" type="BorderPane" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
<top> <top>
<Button fx:id="closeButton" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" opacity="0.5" prefHeight="16.0" prefWidth="16.0" styleClass="closeButton" BorderPane.alignment="CENTER_RIGHT"> <Button fx:id="closeButton" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" opacity="0.5" prefHeight="16.0" prefWidth="16.0" styleClass="closeButton" BorderPane.alignment="CENTER_RIGHT">
<graphic> <graphic>
@ -22,7 +25,7 @@
<Insets bottom="3.0" left="3.0" right="3.0" top="3.0" /> <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
</padding> </padding>
<bottom> <bottom>
<BorderPane fx:id="bottomBorder" minHeight="32.0" BorderPane.alignment="BOTTOM_LEFT"> <BorderPane fx:id="bottomBorder" minHeight="32.0" pickOnBounds="false" BorderPane.alignment="BOTTOM_CENTER">
<left> <left>
<HBox alignment="BOTTOM_LEFT" BorderPane.alignment="BOTTOM_LEFT"> <HBox alignment="BOTTOM_LEFT" BorderPane.alignment="BOTTOM_LEFT">
<children> <children>

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2014-15 Basis Technology Corp. * Copyright 2014-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");
@ -26,6 +26,7 @@ import javafx.fxml.FXML;
import javafx.geometry.Point2D; import javafx.geometry.Point2D;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.chart.Axis;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
@ -34,6 +35,8 @@ import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import org.controlsfx.control.action.Action; import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils; import org.controlsfx.control.action.ActionUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -53,7 +56,7 @@ import org.sleuthkit.autopsy.timeline.TimeLineController;
*/ */
public abstract class IntervalSelector<X> extends BorderPane { public abstract class IntervalSelector<X> extends BorderPane {
private static final Image ClEAR_INTERVAL_ICON = new Image("/org/sleuthkit/autopsy/timeline/images/cross-script.png", 16, 16, true, true, true); //NON-NLS private static final Image CLEAR_INTERVAL_ICON = new Image("/org/sleuthkit/autopsy/timeline/images/cross-script.png", 16, 16, true, true, true); //NON-NLS
private static final Image ZOOM_TO_INTERVAL_ICON = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png", 16, 16, true, true, true); //NON-NLS private static final Image ZOOM_TO_INTERVAL_ICON = new Image("/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png", 16, 16, true, true, true); //NON-NLS
private static final double STROKE_WIDTH = 3; private static final double STROKE_WIDTH = 3;
private static final double HALF_STROKE = STROKE_WIDTH / 2; private static final double HALF_STROKE = STROKE_WIDTH / 2;
@ -61,7 +64,7 @@ public abstract class IntervalSelector<X> extends BorderPane {
/** /**
* the Axis this is a selector over * the Axis this is a selector over
*/ */
public final TimeLineChart<X> chart; public final IntervalSelectorProvider<X> chart;
private Tooltip tooltip; private Tooltip tooltip;
/////////drag state /////////drag state
@ -89,7 +92,7 @@ public abstract class IntervalSelector<X> extends BorderPane {
@FXML @FXML
private BorderPane bottomBorder; private BorderPane bottomBorder;
public IntervalSelector(TimeLineChart<X> chart) { public IntervalSelector(IntervalSelectorProvider<X> chart) {
this.chart = chart; this.chart = chart;
this.controller = chart.getController(); this.controller = chart.getController();
FXMLConstructor.construct(this, IntervalSelector.class, "IntervalSelector.fxml"); // NON-NLS FXMLConstructor.construct(this, IntervalSelector.class, "IntervalSelector.fxml"); // NON-NLS
@ -107,7 +110,7 @@ public abstract class IntervalSelector<X> extends BorderPane {
setMaxWidth(USE_PREF_SIZE); setMaxWidth(USE_PREF_SIZE);
setMinWidth(USE_PREF_SIZE); setMinWidth(USE_PREF_SIZE);
BooleanBinding showingControls = hoverProperty().and(isDragging.not()); BooleanBinding showingControls = zoomButton.hoverProperty().or(bottomBorder.hoverProperty().or(hoverProperty())).and(isDragging.not());
closeButton.visibleProperty().bind(showingControls); closeButton.visibleProperty().bind(showingControls);
closeButton.managedProperty().bind(showingControls); closeButton.managedProperty().bind(showingControls);
zoomButton.visibleProperty().bind(showingControls); zoomButton.visibleProperty().bind(showingControls);
@ -115,8 +118,9 @@ public abstract class IntervalSelector<X> extends BorderPane {
widthProperty().addListener(o -> { widthProperty().addListener(o -> {
IntervalSelector.this.updateStartAndEnd(); IntervalSelector.this.updateStartAndEnd();
if (startLabel.getWidth() + zoomButton.getWidth() + endLabel.getWidth() > getWidth()) { if (startLabel.getWidth() + zoomButton.getWidth() + endLabel.getWidth() > getWidth() - 10) {
this.setCenter(zoomButton); this.setCenter(zoomButton);
bottomBorder.setCenter(new Rectangle(10, 10, Color.TRANSPARENT));
} else { } else {
bottomBorder.setCenter(zoomButton); bottomBorder.setCenter(zoomButton);
} }
@ -154,7 +158,11 @@ public abstract class IntervalSelector<X> extends BorderPane {
mousePress.consume(); mousePress.consume();
}); });
setOnMouseReleased(mouseRelease -> isDragging.set(false)); setOnMouseReleased((MouseEvent mouseRelease) -> {
isDragging.set(false);
mouseRelease.consume();;
});
setOnMouseDragged(mouseDrag -> { setOnMouseDragged(mouseDrag -> {
isDragging.set(true); isDragging.set(true);
double dX = mouseDrag.getScreenX() - startDragX; double dX = mouseDrag.getScreenX() - startDragX;
@ -188,20 +196,17 @@ public abstract class IntervalSelector<X> extends BorderPane {
mouseDrag.consume(); mouseDrag.consume();
}); });
ActionUtils.configureButton(new ZoomToSelectedIntervalAction(), zoomButton); setOnMouseClicked(mouseClick -> {
ActionUtils.configureButton(new ClearSelectedIntervalAction(), closeButton); if (mouseClick.getButton() == MouseButton.SECONDARY) {
//have to add handler rather than use convenience methods so that charts can listen for dismisal click
setOnMouseClicked(mosueClick -> {
if (mosueClick.getButton() == MouseButton.SECONDARY) {
chart.clearIntervalSelector(); chart.clearIntervalSelector();
mosueClick.consume(); } else if (mouseClick.getClickCount() >= 2) {
}
if (mosueClick.getClickCount() >= 2) {
zoomToSelectedInterval(); zoomToSelectedInterval();
mosueClick.consume(); mouseClick.consume();
} }
}); });
ActionUtils.configureButton(new ZoomToSelectedIntervalAction(), zoomButton);
ActionUtils.configureButton(new ClearSelectedIntervalAction(), closeButton);
} }
private Point2D getLocalMouseCoords(MouseEvent mouseEvent) { private Point2D getLocalMouseCoords(MouseEvent mouseEvent) {
@ -246,7 +251,7 @@ public abstract class IntervalSelector<X> extends BorderPane {
@NbBundle.Messages(value = {"# {0} - start timestamp", @NbBundle.Messages(value = {"# {0} - start timestamp",
"# {1} - end timestamp", "# {1} - end timestamp",
"Timeline.ui.TimeLineChart.tooltip.text=Double-click to zoom into range:\n{0} to {1}\nRight-click to clear."}) "Timeline.ui.TimeLineChart.tooltip.text=Double-click to zoom into range:\n{0} to {1}.\n\nRight-click to close."})
private void updateStartAndEnd() { private void updateStartAndEnd() {
String startString = formatSpan(getSpanStart()); String startString = formatSpan(getSpanStart());
String endString = formatSpan(getSpanEnd()); String endString = formatSpan(getSpanEnd());
@ -307,10 +312,35 @@ public abstract class IntervalSelector<X> extends BorderPane {
ClearSelectedIntervalAction() { ClearSelectedIntervalAction() {
super(""); super("");
setLongText(Bundle.IntervalSelector_ClearSelectedIntervalAction_tooltTipText()); setLongText(Bundle.IntervalSelector_ClearSelectedIntervalAction_tooltTipText());
setGraphic(new ImageView(ClEAR_INTERVAL_ICON)); setGraphic(new ImageView(CLEAR_INTERVAL_ICON));
setEventHandler((ActionEvent t) -> { setEventHandler((ActionEvent t) -> {
chart.clearIntervalSelector(); chart.clearIntervalSelector();
}); });
} }
} }
public interface IntervalSelectorProvider<X> {
public TimeLineController getController();
IntervalSelector<? extends X> getIntervalSelector();
void setIntervalSelector(IntervalSelector<? extends X> newIntervalSelector);
/**
* derived classes should implement this so as to supply an appropriate
* subclass of {@link IntervalSelector}
*
* @return a new interval selector
*/
IntervalSelector<X> newIntervalSelector();
/**
* clear any references to previous interval selectors , including
* removing the interval selector from the ui / scene-graph
*/
void clearIntervalSelector();
public Axis<X> getXAxis();
}
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2014-15 Basis Technology Corp. * Copyright 2014-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,28 +18,32 @@
*/ */
package org.sleuthkit.autopsy.timeline.ui; package org.sleuthkit.autopsy.timeline.ui;
import javafx.collections.ObservableList;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.event.EventType; import javafx.event.EventType;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.chart.Axis; import javafx.scene.chart.Axis;
import javafx.scene.chart.Chart;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import org.controlsfx.control.action.ActionGroup; import org.controlsfx.control.action.ActionGroup;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.actions.Back; import org.sleuthkit.autopsy.timeline.actions.Back;
import org.sleuthkit.autopsy.timeline.actions.Forward; import org.sleuthkit.autopsy.timeline.actions.Forward;
import org.sleuthkit.autopsy.timeline.ui.IntervalSelector.IntervalSelectorProvider;
/** /**
* Interface for TimeLineViews that are 'charts'. * Interface for TimeLineViews that are 'charts'.
* *
* @param <X> the type of values along the horizontal axis * @param <X> the type of values along the horizontal axis
*/ */
public interface TimeLineChart<X> { public interface TimeLineChart<X> extends ContextMenuProvider, IntervalSelectorProvider<X> {
ObservableList<? extends Node> getSelectedNodes();
// void setController(TimeLineController controller);
IntervalSelector<? extends X> getIntervalSelector(); IntervalSelector<? extends X> getIntervalSelector();
void setIntervalSelector(IntervalSelector<? extends X> newIntervalSelector); void setIntervalSelector(IntervalSelector<? extends X> newIntervalSelector);
@ -62,10 +66,6 @@ public interface TimeLineChart<X> {
public TimeLineController getController(); public TimeLineController getController();
ContextMenu getChartContextMenu();
ContextMenu getChartContextMenu(MouseEvent m);
/** /**
* drag handler class used by {@link TimeLineChart}s to create * drag handler class used by {@link TimeLineChart}s to create
* {@link IntervalSelector}s * {@link IntervalSelector}s
@ -73,7 +73,7 @@ public interface TimeLineChart<X> {
* @param <X> the type of values along the horizontal axis * @param <X> the type of values along the horizontal axis
* @param <Y> the type of chart this is a drag handler for * @param <Y> the type of chart this is a drag handler for
*/ */
static class ChartDragHandler<X, Y extends Chart & TimeLineChart<X>> implements EventHandler<MouseEvent> { public static class ChartDragHandler<X, Y extends Region & IntervalSelectorProvider<X>> implements EventHandler<MouseEvent> {
private final Y chart; private final Y chart;
@ -97,16 +97,14 @@ public interface TimeLineChart<X> {
chart.getIntervalSelector().prefHeightProperty().bind(chart.heightProperty()); chart.getIntervalSelector().prefHeightProperty().bind(chart.heightProperty());
startX = mouseEvent.getX(); startX = mouseEvent.getX();
chart.getIntervalSelector().relocate(startX, 0); chart.getIntervalSelector().relocate(startX, 0);
} else { } else if (mouseEvent.getX() > startX) {
//resize/position existing selector //resize/position existing selector
if (mouseEvent.getX() > startX) {
chart.getIntervalSelector().relocate(startX, 0); chart.getIntervalSelector().relocate(startX, 0);
chart.getIntervalSelector().setPrefWidth(mouseEvent.getX() - startX); chart.getIntervalSelector().setPrefWidth(mouseEvent.getX() - startX);
} else { } else {
chart.getIntervalSelector().relocate(mouseEvent.getX(), 0); chart.getIntervalSelector().relocate(mouseEvent.getX(), 0);
chart.getIntervalSelector().setPrefWidth(startX - mouseEvent.getX()); chart.getIntervalSelector().setPrefWidth(startX - mouseEvent.getX());
} }
}
chart.getIntervalSelector().autosize(); chart.getIntervalSelector().autosize();
} else if (mouseEventType == MouseEvent.MOUSE_RELEASED) { } else if (mouseEventType == MouseEvent.MOUSE_RELEASED) {
chart.setCursor(Cursor.DEFAULT); chart.setCursor(Cursor.DEFAULT);
@ -114,10 +112,9 @@ public interface TimeLineChart<X> {
chart.setCursor(Cursor.DEFAULT); chart.setCursor(Cursor.DEFAULT);
} }
} }
} }
static class MouseClickedHandler<X, C extends Chart & TimeLineChart<X>> implements EventHandler<MouseEvent> { static class MouseClickedHandler<X, C extends Region & TimeLineChart<X>> implements EventHandler<MouseEvent> {
private final C chart; private final C chart;
@ -126,29 +123,17 @@ public interface TimeLineChart<X> {
} }
@Override @Override
public void handle(MouseEvent clickEvent) { public void handle(MouseEvent mouseEvent) {
if (chart.getChartContextMenu() != null) { if (MouseEvent.MOUSE_CLICKED == mouseEvent.getEventType() && mouseEvent.isPopupTrigger() && mouseEvent.isStillSincePress()) {
chart.getChartContextMenu().hide(); ContextMenu chartContextMenu = chart.getContextMenu(mouseEvent);
chartContextMenu.show(chart, mouseEvent.getScreenX(), mouseEvent.getScreenY());
chart.clearContextMenu();
} else if (mouseEvent.getButton() == MouseButton.PRIMARY && mouseEvent.isStillSincePress()) {
chart.getSelectedNodes().clear();
} }
if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) { mouseEvent.consume();
chart.getChartContextMenu(clickEvent);
chart.setOnMouseMoved(this);
chart.getChartContextMenu().show(chart, clickEvent.getScreenX(), clickEvent.getScreenY());
clickEvent.consume();
} }
} }
}
/**
* enum to represent whether the drag is a left/right-edge modification or a
* horizontal slide triggered by dragging the center
*/
enum DragPosition {
LEFT,
CENTER,
RIGHT
}
@NbBundle.Messages({"TimeLineChart.zoomHistoryActionGroup.name=Zoom History"}) @NbBundle.Messages({"TimeLineChart.zoomHistoryActionGroup.name=Zoom History"})
static ActionGroup newZoomHistoyActionGroup(TimeLineController controller) { static ActionGroup newZoomHistoyActionGroup(TimeLineController controller) {

View File

@ -383,7 +383,10 @@ final public class VisualizationPanel extends BorderPane {
notificationPane.setContent(visualization); notificationPane.setContent(visualization);
if (visualization instanceof DetailViewPane) { if (visualization instanceof DetailViewPane) {
Platform.runLater(() -> {
((DetailViewPane) visualization).setHighLightedEvents(eventsTree.getSelectedEvents());
eventsTree.setDetailViewPane((DetailViewPane) visualization); eventsTree.setDetailViewPane((DetailViewPane) visualization);
});
} }
visualization.hasEvents.addListener((observable, oldValue, newValue) -> { visualization.hasEvents.addListener((observable, oldValue, newValue) -> {
if (newValue == false) { if (newValue == false) {

View File

@ -45,6 +45,7 @@ import javafx.scene.layout.Region;
import org.joda.time.Interval; import org.joda.time.Interval;
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.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.FXMLConstructor;
import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
@ -102,7 +103,7 @@ public class CountsViewPane extends AbstractVisualizationPane<String, Number, No
public CountsViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region spacer) { public CountsViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region spacer) {
super(controller, partPane, contextPane, spacer); super(controller, partPane, contextPane, spacer);
chart = new EventCountsChart(controller, dateAxis, countAxis, selectedNodes); chart = new EventCountsChart(controller, dateAxis, countAxis, selectedNodes);
setChartClickHandler();
chart.setData(dataSeries); chart.setData(dataSeries);
setCenter(chart); setCenter(chart);
@ -201,10 +202,9 @@ public class CountsViewPane extends AbstractVisualizationPane<String, Number, No
} }
} }
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@Override @Override
protected void resetData() { protected void resetData() {
Platform.runLater(() -> {
for (XYChart.Series<String, Number> s : dataSeries) { for (XYChart.Series<String, Number> s : dataSeries) {
s.getData().clear(); s.getData().clear();
} }
@ -212,7 +212,6 @@ public class CountsViewPane extends AbstractVisualizationPane<String, Number, No
dataSeries.clear(); dataSeries.clear();
eventTypeToSeriesMap.clear(); eventTypeToSeriesMap.clear();
createSeries(); createSeries();
});
} }
private static enum ScaleType implements Function<Long, Double> { private static enum ScaleType implements Function<Long, Double> {

View File

@ -19,7 +19,6 @@
package org.sleuthkit.autopsy.timeline.ui.countsview; package org.sleuthkit.autopsy.timeline.ui.countsview;
import java.util.Arrays; import java.util.Arrays;
import java.util.MissingResourceException;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.event.EventHandler; import javafx.event.EventHandler;
@ -65,11 +64,6 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
private static final Effect SELECTED_NODE_EFFECT = new Lighting(); private static final Effect SELECTED_NODE_EFFECT = new Lighting();
private ContextMenu chartContextMenu; private ContextMenu chartContextMenu;
@Override
public ContextMenu getChartContextMenu() {
return chartContextMenu;
}
private final TimeLineController controller; private final TimeLineController controller;
private final FilteredEventsModel filteredEvents; private final FilteredEventsModel filteredEvents;
@ -114,6 +108,10 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
setOnMouseClicked(new MouseClickedHandler<>(this)); setOnMouseClicked(new MouseClickedHandler<>(this));
this.selectedNodes = selectedNodes; this.selectedNodes = selectedNodes;
getController().getEventsModel().timeRangeProperty().addListener(o -> {
clearIntervalSelector();
});
} }
@Override @Override
@ -122,8 +120,22 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
intervalSelector = null; intervalSelector = null;
} }
public void clearContextMenu() {
chartContextMenu = null;
}
/**
* used by {@link CountsViewPane#BarClickHandler} to close the context menu
* when the bar menu is requested
*
* @return the context menu for this chart
*/
public ContextMenu getContextMenu() {
return chartContextMenu;
}
@Override @Override
public ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException { public ContextMenu getContextMenu(MouseEvent clickEvent) {
if (chartContextMenu != null) { if (chartContextMenu != null) {
chartContextMenu.hide(); chartContextMenu.hide();
} }
@ -155,14 +167,8 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
return new CountsIntervalSelector(this); return new CountsIntervalSelector(this);
} }
/** public ObservableList<Node> getSelectedNodes() {
* used by {@link CountsViewPane#BarClickHandler} to close the context menu return selectedNodes;
* when the bar menu is requested
*
* @return the context menu for this chart
*/
ContextMenu getContextMenu() {
return chartContextMenu;
} }
void setRangeInfo(RangeDivisionInfo rangeInfo) { void setRangeInfo(RangeDivisionInfo rangeInfo) {
@ -370,7 +376,7 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
controller.selectTimeAndType(interval, type); controller.selectTimeAndType(interval, type);
selectedNodes.setAll(node); selectedNodes.setAll(node);
} else if (e.getButton().equals(MouseButton.SECONDARY)) { } else if (e.getButton().equals(MouseButton.SECONDARY)) {
getChartContextMenu(e).hide(); getContextMenu(e).hide();
if (barContextMenu == null) { if (barContextMenu == null) {
barContextMenu = new ContextMenu(); barContextMenu = new ContextMenu();
@ -382,7 +388,7 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
new SeparatorMenuItem(), new SeparatorMenuItem(),
ActionUtils.createMenuItem(new ZoomToIntervalAction())); ActionUtils.createMenuItem(new ZoomToIntervalAction()));
barContextMenu.getItems().addAll(getChartContextMenu(e).getItems()); barContextMenu.getItems().addAll(getContextMenu(e).getItems());
} }
barContextMenu.show(node, e.getScreenX(), e.getScreenY()); barContextMenu.show(node, e.getScreenX(), e.getScreenY());

View File

@ -4,8 +4,8 @@ DetailViewPane.bandByTypeBox.text=Band by Type
DetailViewPane.bandByTypeBoxMenuItem.text=Band by Type DetailViewPane.bandByTypeBoxMenuItem.text=Band by Type
DetailViewPane.oneEventPerRowBox.text=One Per Row DetailViewPane.oneEventPerRowBox.text=One Per Row
DetailViewPane.oneEventPerRowBoxMenuItem.text=One Per Row DetailViewPane.oneEventPerRowBoxMenuItem.text=One Per Row
DetailViewPan.truncateAllBox.text=Truncate Descriptions DetailViewPane.truncateAllBox.text=Truncate Descriptions
DetailViewPan.truncateAllBoxMenuItem.text=Truncate Descriptions DetailViewPane.truncateAllBoxMenuItem.text=Truncate Descriptions
DetailViewPane.truncateSlideLabelMenuItem.text=max description width (px) DetailViewPane.truncateSlideLabelMenuItem.text=max description width (px)
DetailViewPane.descVisSeparatorMenuItem.text=Description Visibility DetailViewPane.descVisSeparatorMenuItem.text=Description Visibility
DetailViewPane.showRadioMenuItem.text=Show Full Description DetailViewPane.showRadioMenuItem.text=Show Full Description
@ -14,3 +14,4 @@ DetailViewPane.countsRadioMenuItem.text=Show Counts Only
DetailViewPane.countsRadio.text=Show Counts Only DetailViewPane.countsRadio.text=Show Counts Only
DetailViewPane.hiddenRadioMenuItem.text=Hide Description DetailViewPane.hiddenRadioMenuItem.text=Hide Description
DetailViewPane.hiddenRadio.text=Hide Description DetailViewPane.hiddenRadio.text=Hide Description

View File

@ -51,7 +51,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
*/ */
final class DateAxis extends Axis<DateTime> { final class DateAxis extends Axis<DateTime> {
private ObjectProperty<DateTime> lowerBound = new ObjectPropertyBase<DateTime>() { private ObjectProperty<DateTime> lowerBound = new ObjectPropertyBase<DateTime>(new DateTime(0)) {
@Override @Override
protected void invalidated() { protected void invalidated() {
if (!isAutoRanging()) { if (!isAutoRanging()) {
@ -89,7 +89,7 @@ final class DateAxis extends Axis<DateTime> {
private final ReadOnlyDoubleWrapper tickSpacing = new ReadOnlyDoubleWrapper(); private final ReadOnlyDoubleWrapper tickSpacing = new ReadOnlyDoubleWrapper();
private final ObjectProperty<DateTime> upperBound = new ObjectPropertyBase<DateTime>() { private final ObjectProperty<DateTime> upperBound = new ObjectPropertyBase<DateTime>(new DateTime(1)) {
@Override @Override
protected void invalidated() { protected void invalidated() {
if (!isAutoRanging()) { if (!isAutoRanging()) {
@ -114,7 +114,11 @@ final class DateAxis extends Axis<DateTime> {
* by the data. * by the data.
*/ */
DateAxis() { DateAxis() {
setTickLabelGap(0);
setAutoRanging(false); setAutoRanging(false);
setTickLabelsVisible(false);
setTickLength(0);
setTickMarkVisible(false);
} }
@Override @Override
@ -147,7 +151,7 @@ final class DateAxis extends Axis<DateTime> {
* *
* @see #lowerBoundProperty() * @see #lowerBoundProperty()
*/ */
public final DateTime getLowerBound() { public DateTime getLowerBound() {
return lowerBound.get(); return lowerBound.get();
} }
@ -158,7 +162,7 @@ final class DateAxis extends Axis<DateTime> {
* *
* @see #lowerBoundProperty() * @see #lowerBoundProperty()
*/ */
public final void setLowerBound(DateTime date) { public void setLowerBound(DateTime date) {
lowerBound.set(date); lowerBound.set(date);
} }
@ -169,7 +173,7 @@ final class DateAxis extends Axis<DateTime> {
* *
* @see #upperBoundProperty() * @see #upperBoundProperty()
*/ */
public final DateTime getUpperBound() { public DateTime getUpperBound() {
return upperBound.get(); return upperBound.get();
} }
@ -180,7 +184,7 @@ final class DateAxis extends Axis<DateTime> {
* *
* @see #upperBoundProperty() () * @see #upperBoundProperty() ()
*/ */
public final void setUpperBound(DateTime date) { public void setUpperBound(DateTime date) {
upperBound.set(date); upperBound.set(date);
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2014-15 Basis Technology Corp. * Copyright 2014-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,17 +20,16 @@ package org.sleuthkit.autopsy.timeline.ui.detailview;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.geometry.Orientation;
import javafx.scene.chart.Axis; import javafx.scene.chart.Axis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
@ -38,194 +37,127 @@ import javafx.scene.control.CheckBox;
import javafx.scene.control.CustomMenuItem; import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.MenuButton; import javafx.scene.control.MenuButton;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Slider; import javafx.scene.control.Slider;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.control.TreeItem;
import javafx.scene.effect.Effect; import javafx.scene.effect.Effect;
import static javafx.scene.input.KeyCode.DOWN;
import static javafx.scene.input.KeyCode.KP_DOWN;
import static javafx.scene.input.KeyCode.KP_UP;
import static javafx.scene.input.KeyCode.PAGE_DOWN;
import static javafx.scene.input.KeyCode.PAGE_UP;
import static javafx.scene.input.KeyCode.UP;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Modality; import javafx.stage.Modality;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.action.Action; import org.controlsfx.control.action.Action;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.Interval; import org.joda.time.Interval;
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.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.FXMLConstructor;
import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane;
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChart.HideDescriptionAction;
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChart.UnhideDescriptionAction;
import org.sleuthkit.autopsy.timeline.utils.MappedList;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
/** /**
* Controller class for a {@link EventDetailsChart} based implementation of a * Controller class for a {@link PrimaryDetailsChart} based implementation of a
* TimeLineView. * TimeLineView.
* *
* This class listens to changes in the assigned {@link FilteredEventsModel} and * This class listens to changes in the assigned {@link FilteredEventsModel} and
* updates the internal {@link EventDetailsChart} to reflect the currently * updates the internal {@link PrimaryDetailsChart} to reflect the currently
* requested events. * requested events.
* *
* Concurrency Policy: Access to the private members clusterChart, dateAxis, * Concurrency Policy: Access to the private members clusterChart, dateAxis,
* EventTypeMap, and dataSets is all linked directly to the ClusterChart which * EventTypeMap, and dataSets is all linked directly to the ClusterChart which
* must only be manipulated on the JavaFx thread. * must only be manipulated on the JavaFx thread.
*/ */
public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStripe, EventBundleNodeBase<?, ?, ?>, EventDetailsChart> { public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStripe, EventNodeBase<?>, DetailsChart> {
private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName()); private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName());
private static final double LINE_SCROLL_PERCENTAGE = .10; private final DateAxis detailsChartDateAxis = new DateAxis();
private static final double PAGE_SCROLL_PERCENTAGE = .70; private final DateAxis pinnedDateAxis = new DateAxis();
private final DateAxis dateAxis = new DateAxis(); @NbBundle.Messages("DetailViewPane.primaryLaneLabel.text=All Events (Filtered)")
private final Axis<EventStripe> verticalAxis = new EventAxis(); private final Axis<EventStripe> verticalAxis = new EventAxis<>(Bundle.DetailViewPane_primaryLaneLabel_text());
private final ScrollBar vertScrollBar = new ScrollBar();
private final Region scrollBarSpacer = new Region();
private MultipleSelectionModel<TreeItem<EventBundle<?>>> treeSelectionModel; private final MappedList<TimeLineEvent, EventNodeBase<?>> selectedEvents;
private final ObservableList<EventBundleNodeBase<?, ?, ?>> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
public ObservableList<EventStripe> getEventStripes() {
return chart.getEventStripes();
}
@Override
protected void resetData() {
for (XYChart.Series<DateTime, EventStripe> s : dataSeries) {
s.getData().forEach(chart::removeDataItem);
s.getData().clear();
}
Platform.runLater(() -> {
vertScrollBar.setValue(0);
});
}
public DetailViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region bottomLeftSpacer) { public DetailViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region bottomLeftSpacer) {
super(controller, partPane, contextPane, bottomLeftSpacer); super(controller, partPane, contextPane, bottomLeftSpacer);
this.selectedEvents = new MappedList<>(getSelectedNodes(), EventNodeBase<?>::getEvent);
//initialize chart; //initialize chart;
chart = new EventDetailsChart(controller, dateAxis, verticalAxis, selectedNodes); chart = new DetailsChart(controller, detailsChartDateAxis, pinnedDateAxis, verticalAxis, getSelectedNodes());
setChartClickHandler(); //can we push this into chart
chart.setData(dataSeries);
setCenter(chart); setCenter(chart);
settingsNodes = new ArrayList<>(new DetailViewSettingsPane(chart.getLayoutSettings()).getChildrenUnmodifiable());
settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable()); // //bind layout fo axes and spacers
//bind layout fo axes and spacers detailsChartDateAxis.getTickMarks().addListener((Observable observable) -> layoutDateLabels());
dateAxis.setTickLabelGap(0); detailsChartDateAxis.getTickSpacing().addListener(observable -> layoutDateLabels());
dateAxis.setAutoRanging(false);
dateAxis.setTickLabelsVisible(false);
dateAxis.getTickMarks().addListener((Observable observable) -> layoutDateLabels());
dateAxis.getTickSpacing().addListener(observable -> layoutDateLabels());
verticalAxis.setAutoRanging(false); //prevent XYChart.updateAxisRange() from accessing dataSeries on JFX thread causing ConcurrentModificationException verticalAxis.setAutoRanging(false); //prevent XYChart.updateAxisRange() from accessing dataSeries on JFX thread causing ConcurrentModificationException
bottomLeftSpacer.minWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); bottomLeftSpacer.minWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty()));
bottomLeftSpacer.prefWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); bottomLeftSpacer.prefWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty()));
bottomLeftSpacer.maxWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); bottomLeftSpacer.maxWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty()));
scrollBarSpacer.minHeightProperty().bind(dateAxis.heightProperty());
//configure scrollbar
vertScrollBar.setOrientation(Orientation.VERTICAL);
vertScrollBar.maxProperty().bind(chart.maxVScrollProperty().subtract(chart.heightProperty()));
vertScrollBar.visibleAmountProperty().bind(chart.heightProperty());
vertScrollBar.visibleProperty().bind(vertScrollBar.visibleAmountProperty().greaterThanOrEqualTo(0));
VBox.setVgrow(vertScrollBar, Priority.ALWAYS);
setRight(new VBox(vertScrollBar, scrollBarSpacer));
//interpret scroll events to the scrollBar
this.setOnScroll(scrollEvent ->
vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() - scrollEvent.getDeltaY())));
//request focus for keyboard scrolling
setOnMouseClicked(mouseEvent -> requestFocus());
//interpret scroll related keys to scrollBar
this.setOnKeyPressed((KeyEvent t) -> {
switch (t.getCode()) {
case PAGE_UP:
incrementScrollValue(-PAGE_SCROLL_PERCENTAGE);
t.consume();
break;
case PAGE_DOWN:
incrementScrollValue(PAGE_SCROLL_PERCENTAGE);
t.consume();
break;
case KP_UP:
case UP:
incrementScrollValue(-LINE_SCROLL_PERCENTAGE);
t.consume();
break;
case KP_DOWN:
case DOWN:
incrementScrollValue(LINE_SCROLL_PERCENTAGE);
t.consume();
break;
}
});
//scrollbar value change handler. This forwards changes in scroll bar to chart
this.vertScrollBar.valueProperty().addListener(observable -> chart.setVScroll(vertScrollBar.getValue()));
//maintain highlighted effect on correct nodes
highlightedNodes.addListener((ListChangeListener.Change<? extends EventBundleNodeBase<?, ?, ?>> change) -> {
while (change.next()) {
change.getAddedSubList().forEach(node -> {
node.applyHighlightEffect(true);
});
change.getRemoved().forEach(node -> {
node.applyHighlightEffect(false);
});
}
});
selectedNodes.addListener((Observable observable) -> { selectedNodes.addListener((Observable observable) -> {
highlightedNodes.clear(); chart.setHighlightPredicate(selectedNodes::contains);
selectedNodes.stream().forEach((tn) -> { getController().selectEventIDs(selectedNodes.stream()
for (EventBundleNodeBase<?, ?, ?> n : chart.getNodes((EventBundleNodeBase<?, ?, ?> t) -> .flatMap(detailNode -> detailNode.getEventIDs().stream())
t.getDescription().equals(tn.getDescription()))) { .collect(Collectors.toList()));
highlightedNodes.add(n);
}
});
}); });
} }
private void incrementScrollValue(double factor) { public ObservableList<EventStripe> getAllEventStripes() {
vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() + factor * chart.getHeight())); return chart.getAllNestedEventStripes();
} }
private Double clampScroll(Double value) { public ObservableList<TimeLineEvent> getSelectedEvents() {
return Math.max(0, Math.min(vertScrollBar.getMax() + 50, value)); return selectedEvents;
} }
public void setSelectionModel(MultipleSelectionModel<TreeItem<EventBundle<?>>> selectionModel) { public void setHighLightedEvents(ObservableList<TimeLineEvent> highlightedEvents) {
this.treeSelectionModel = selectionModel; highlightedEvents.addListener((Observable observable) -> {
Predicate<EventNodeBase<?>> highlightPredicate =
treeSelectionModel.getSelectedItems().addListener((Observable observable) -> { highlightedEvents.stream()
highlightedNodes.clear(); .map(TimeLineEvent::getDescription)
for (TreeItem<EventBundle<?>> tn : treeSelectionModel.getSelectedItems()) { .map(new Function<String, Predicate<EventNodeBase<?>>>() {
@Override
for (EventBundleNodeBase<?, ?, ?> n : chart.getNodes((EventBundleNodeBase<?, ?, ?> t) -> public Predicate<EventNodeBase<?>> apply(String description) {
t.getDescription().equals(tn.getValue().getDescription()))) { return eventNode -> StringUtils.equalsIgnoreCase(eventNode.getDescription(), description);
highlightedNodes.add(n);
}
} }
})
.reduce(selectedNodes::contains, Predicate::or);
chart.setHighlightPredicate(highlightPredicate);
}); });
} }
@Override
public Axis<DateTime> getXAxis() {
return detailsChartDateAxis;
}
public Action newUnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
return new UnhideDescriptionAction(description, descriptionLoD, chart);
}
public Action newHideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
return new HideDescriptionAction(description, descriptionLoD, chart);
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@Override
protected void resetData() {
chart.reset();
}
@Override @Override
protected Boolean isTickBold(DateTime value) { protected Boolean isTickBold(DateTime value) {
return false; return false;
@ -236,19 +168,14 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
return verticalAxis; return verticalAxis;
} }
@Override
protected Axis<DateTime> getXAxis() {
return dateAxis;
}
@Override @Override
protected double getTickSpacing() { protected double getTickSpacing() {
return dateAxis.getTickSpacing().get(); return detailsChartDateAxis.getTickSpacing().get();
} }
@Override @Override
protected String getTickMarkLabel(DateTime value) { protected String getTickMarkLabel(DateTime value) {
return dateAxis.getTickMarkLabel(value); return detailsChartDateAxis.getTickMarkLabel(value);
} }
@Override @Override
@ -262,11 +189,17 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
} }
@Override @Override
protected void applySelectionEffect(EventBundleNodeBase<?, ?, ?> c1, Boolean selected) { protected void applySelectionEffect(EventNodeBase<?> c1, Boolean selected) {
c1.applySelectionEffect(selected); c1.applySelectionEffect(selected);
} }
private class DetailViewSettingsPane extends HBox { DateTime getDateTimeForPosition(double layoutX) {
return chart.getDateTimeForPosition(layoutX);
}
static private class DetailViewSettingsPane extends HBox {
@FXML @FXML
private RadioButton hiddenRadio; private RadioButton hiddenRadio;
@ -322,7 +255,12 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
@FXML @FXML
private SeparatorMenuItem descVisibilitySeparatorMenuItem; private SeparatorMenuItem descVisibilitySeparatorMenuItem;
DetailViewSettingsPane() { @FXML
private ToggleButton pinnedEventsToggle;
private final DetailsChartLayoutSettings layoutSettings;
DetailViewSettingsPane(DetailsChartLayoutSettings layoutSettings) {
this.layoutSettings = layoutSettings;
FXMLConstructor.construct(DetailViewSettingsPane.this, "DetailViewSettingsPane.fxml"); // NON-NLS FXMLConstructor.construct(DetailViewSettingsPane.this, "DetailViewSettingsPane.fxml"); // NON-NLS
} }
@ -332,14 +270,16 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
assert oneEventPerRowBox != null : "fx:id=\"oneEventPerRowBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS assert oneEventPerRowBox != null : "fx:id=\"oneEventPerRowBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
assert truncateAllBox != null : "fx:id=\"truncateAllBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS assert truncateAllBox != null : "fx:id=\"truncateAllBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
assert truncateWidthSlider != null : "fx:id=\"truncateAllSlider\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS assert truncateWidthSlider != null : "fx:id=\"truncateAllSlider\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
bandByTypeBox.selectedProperty().bindBidirectional(chart.bandByTypeProperty()); assert pinnedEventsToggle != null : "fx:id=\"pinnedEventsToggle\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
truncateAllBox.selectedProperty().bindBidirectional(chart.truncateAllProperty()); bandByTypeBox.selectedProperty().bindBidirectional(layoutSettings.bandByTypeProperty());
oneEventPerRowBox.selectedProperty().bindBidirectional(chart.oneEventPerRowProperty()); truncateAllBox.selectedProperty().bindBidirectional(layoutSettings.truncateAllProperty());
oneEventPerRowBox.selectedProperty().bindBidirectional(layoutSettings.oneEventPerRowProperty());
truncateSliderLabel.disableProperty().bind(truncateAllBox.selectedProperty().not()); truncateSliderLabel.disableProperty().bind(truncateAllBox.selectedProperty().not());
truncateSliderLabel.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateSliderLabel.text")); pinnedEventsToggle.selectedProperty().bindBidirectional(layoutSettings.pinnedLaneShowing());
final InvalidationListener sliderListener = o -> { final InvalidationListener sliderListener = o -> {
if (truncateWidthSlider.isValueChanging() == false) { if (truncateWidthSlider.isValueChanging() == false) {
chart.getTruncateWidth().set(truncateWidthSlider.getValue()); layoutSettings.truncateWidthProperty().set(truncateWidthSlider.getValue());
} }
}; };
truncateWidthSlider.valueProperty().addListener(sliderListener); truncateWidthSlider.valueProperty().addListener(sliderListener);
@ -347,29 +287,25 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
descrVisibility.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> { descrVisibility.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> {
if (newToggle == countsRadio) { if (newToggle == countsRadio) {
chart.descrVisibilityProperty().set(DescriptionVisibility.COUNT_ONLY); layoutSettings.descrVisibilityProperty().set(DescriptionVisibility.COUNT_ONLY);
} else if (newToggle == showRadio) { } else if (newToggle == showRadio) {
chart.descrVisibilityProperty().set(DescriptionVisibility.SHOWN); layoutSettings.descrVisibilityProperty().set(DescriptionVisibility.SHOWN);
} else if (newToggle == hiddenRadio) { } else if (newToggle == hiddenRadio) {
chart.descrVisibilityProperty().set(DescriptionVisibility.HIDDEN); layoutSettings.descrVisibilityProperty().set(DescriptionVisibility.HIDDEN);
} }
}); });
advancedLayoutOptionsButtonLabel.setText( truncateSliderLabel.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateSliderLabel.text"));
NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.advancedLayoutOptionsButtonLabel.text"));
advancedLayoutOptionsButtonLabel.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.advancedLayoutOptionsButtonLabel.text"));
bandByTypeBox.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.bandByTypeBox.text")); bandByTypeBox.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.bandByTypeBox.text"));
bandByTypeBoxMenuItem.setText( bandByTypeBoxMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.bandByTypeBoxMenuItem.text"));
NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.bandByTypeBoxMenuItem.text"));
oneEventPerRowBox.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.oneEventPerRowBox.text")); oneEventPerRowBox.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.oneEventPerRowBox.text"));
oneEventPerRowBoxMenuItem.setText( oneEventPerRowBoxMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.oneEventPerRowBoxMenuItem.text"));
NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.oneEventPerRowBoxMenuItem.text")); truncateAllBox.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateAllBox.text"));
truncateAllBox.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPan.truncateAllBox.text")); truncateAllBoxMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateAllBoxMenuItem.text"));
truncateAllBoxMenuItem.setText( truncateSliderLabelMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateSlideLabelMenuItem.text"));
NbBundle.getMessage(DetailViewPane.class, "DetailViewPan.truncateAllBoxMenuItem.text")); descVisibilitySeparatorMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.descVisSeparatorMenuItem.text"));
truncateSliderLabelMenuItem.setText(
NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateSlideLabelMenuItem.text"));
descVisibilitySeparatorMenuItem.setText(
NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.descVisSeparatorMenuItem.text"));
showRadioMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.showRadioMenuItem.text")); showRadioMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.showRadioMenuItem.text"));
showRadio.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.showRadio.text")); showRadio.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.showRadio.text"));
countsRadioMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.countsRadioMenuItem.text")); countsRadioMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.countsRadioMenuItem.text"));
@ -379,14 +315,6 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
} }
} }
public Action newUnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
return chart.new UnhideDescriptionAction(description, descriptionLoD);
}
public Action newHideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
return chart.new HideDescriptionAction(description, descriptionLoD);
}
@NbBundle.Messages({ @NbBundle.Messages({
"DetailViewPane.loggedTask.queryDb=Retreiving event data", "DetailViewPane.loggedTask.queryDb=Retreiving event data",
"DetailViewPane.loggedTask.name=Updating Details View", "DetailViewPane.loggedTask.name=Updating Details View",
@ -441,12 +369,9 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
return null; return null;
} }
updateProgress(i, size); updateProgress(i, size);
final EventStripe cluster = eventStripes.get(i); final EventStripe stripe = eventStripes.get(i);
final XYChart.Data<DateTime, EventStripe> dataItem = new XYChart.Data<>(new DateTime(cluster.getStartMillis()), cluster); Platform.runLater(() -> chart.addStripe(stripe));
getSeries(cluster.getEventType()).getData().add(dataItem);
chart.addDataItem(dataItem);
} }
return eventStripes.isEmpty() == false; return eventStripes.isEmpty() == false;
} }
@ -458,7 +383,9 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
@Override @Override
protected void setDateAxisValues(Interval timeRange) { protected void setDateAxisValues(Interval timeRange) {
dateAxis.setRange(timeRange, true); detailsChartDateAxis.setRange(timeRange, true);
pinnedDateAxis.setRange(timeRange, true);
} }
} }
} }

View File

@ -1,12 +1,30 @@
<?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.CheckBox?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.CustomMenuItem?>
<?import javafx.scene.layout.*?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuButton?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.SeparatorMenuItem?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?>
<fx:root alignment="CENTER_LEFT" spacing="5.0" type="javafx.scene.layout.HBox" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> <fx:root alignment="CENTER_LEFT" spacing="5.0" type="javafx.scene.layout.HBox" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
<children> <children>
<ToggleButton fx:id="pinnedEventsToggle" mnemonicParsing="false" text="Pinned Events">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../images/marker--pin.png" />
</image>
</ImageView>
</graphic>
</ToggleButton>
<MenuButton fx:id="advancedLayoutOptionsButtonLabel" mnemonicParsing="false"> <MenuButton fx:id="advancedLayoutOptionsButtonLabel" mnemonicParsing="false">
<items> <items>
<CustomMenuItem fx:id="bandByTypeBoxMenuItem" hideOnClick="false" mnemonicParsing="false"> <CustomMenuItem fx:id="bandByTypeBoxMenuItem" hideOnClick="false" mnemonicParsing="false">

View File

@ -0,0 +1,412 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2014-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.timeline.ui.detailview;
import java.util.Arrays;
import java.util.MissingResourceException;
import java.util.function.Predicate;
import javafx.beans.Observable;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import javafx.event.ActionEvent;
import javafx.geometry.Side;
import javafx.scene.chart.Axis;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import org.controlsfx.control.MasterDetailPane;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.openide.util.NbBundle;
import org.python.google.common.collect.Iterables;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
import org.sleuthkit.autopsy.timeline.ui.IntervalSelector;
import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
public final class DetailsChart extends Control implements TimeLineChart<DateTime> {
private final DateAxis detailsChartDateAxis;
private final DateAxis pinnedDateAxis;
private final Axis<EventStripe> verticalAxis;
private final SimpleObjectProperty< IntervalSelector<? extends DateTime>> intervalSelector = new SimpleObjectProperty<>();
private final SimpleObjectProperty<Predicate<EventNodeBase<?>>> highlightPredicate = new SimpleObjectProperty<>((x) -> false);
private final ObservableList<EventNodeBase<?>> selectedNodes;
private final DetailsChartLayoutSettings layoutSettings = new DetailsChartLayoutSettings();
private final TimeLineController controller;
private final ObservableList<EventStripe> nestedEventStripes = FXCollections.observableArrayList();
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final ObservableList<EventStripe> eventStripes = FXCollections.observableArrayList();
DetailsChart(TimeLineController controller, DateAxis detailsChartDateAxis, DateAxis pinnedDateAxis, Axis<EventStripe> verticalAxis, ObservableList<EventNodeBase<?>> selectedNodes) {
this.controller = controller;
this.detailsChartDateAxis = detailsChartDateAxis;
this.verticalAxis = verticalAxis;
this.pinnedDateAxis = pinnedDateAxis;
this.selectedNodes = selectedNodes;
getController().getPinnedEvents().addListener((SetChangeListener.Change<? extends TimeLineEvent> change) -> {
layoutSettings.setPinnedLaneShowing(change.getSet().isEmpty() == false);
});
if (getController().getPinnedEvents().isEmpty() == false) {
layoutSettings.setPinnedLaneShowing(true);
}
getController().getEventsModel().timeRangeProperty().addListener(o -> {
clearGuideLines();
clearIntervalSelector();
});
getController().getEventsModel().zoomParametersProperty().addListener(o -> {
getSelectedNodes().clear();
});
}
DateTime getDateTimeForPosition(double layoutX) {
return ((DetailsChartSkin) getSkin()).getDateTimeForPosition(layoutX);
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
void addStripe(EventStripe stripe) {
eventStripes.add(stripe);
nestedEventStripes.add(stripe);
}
void clearGuideLines() {
guideLines.clear();
}
void clearGuideLine(GuideLine guideLine) {
guideLines.remove(guideLine);
}
public ObservableList<EventNodeBase<?>> getSelectedNodes() {
return selectedNodes;
}
DetailsChartLayoutSettings getLayoutSettings() {
return layoutSettings;
}
void setHighlightPredicate(Predicate<EventNodeBase<?>> highlightPredicate) {
this.highlightPredicate.set(highlightPredicate);
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
void reset() {
eventStripes.clear();
nestedEventStripes.clear();
}
public ObservableList<EventStripe> getAllNestedEventStripes() {
return nestedEventStripes;
}
private static class DetailIntervalSelector extends IntervalSelector<DateTime> {
DetailIntervalSelector(IntervalSelectorProvider<DateTime> chart) {
super(chart);
}
@Override
protected String formatSpan(DateTime date) {
return date.toString(TimeLineController.getZonedFormatter());
}
@Override
protected Interval adjustInterval(Interval i) {
return i;
}
@Override
protected DateTime parseDateTime(DateTime date) {
return date;
}
}
private final ObservableSet<GuideLine> guideLines = FXCollections.observableSet();
private void addGuideLine(GuideLine guideLine) {
guideLines.add(guideLine);
}
static private class PlaceMarkerAction extends Action {
private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); //NON-NLS
private GuideLine guideLine;
@NbBundle.Messages({"PlaceMArkerAction.name=Place Marker"})
PlaceMarkerAction(DetailsChart chart, MouseEvent clickEvent) {
super(Bundle.PlaceMArkerAction_name());
setGraphic(new ImageView(MARKER)); // NON-NLS
setEventHandler(actionEvent -> {
if (guideLine == null) {
guideLine = new GuideLine(chart);
guideLine.relocate(chart.sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0);
chart.addGuideLine(guideLine);
} else {
guideLine.relocate(chart.sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0);
}
});
}
}
/**
*
* @param chartLane the value of chartLane
* @param mouseClickedHandler the value of mouseClickedHandler
* @param chartDragHandler1 the value of chartDragHandler1
*/
static private void configureMouseListeners(final DetailsChartLane<?> chartLane, final TimeLineChart.MouseClickedHandler<DateTime, DetailsChart> mouseClickedHandler, final TimeLineChart.ChartDragHandler<DateTime, DetailsChart> chartDragHandler) {
chartLane.setOnMousePressed(chartDragHandler);
chartLane.setOnMouseReleased(chartDragHandler);
chartLane.setOnMouseDragged(chartDragHandler);
chartLane.setOnMouseClicked(chartDragHandler);
chartLane.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedHandler);
}
public void clearIntervalSelector() {
intervalSelector.set(null);
}
@Override
public IntervalSelector<DateTime> newIntervalSelector() {
return new DetailIntervalSelector(this);
}
@Override
public IntervalSelector<? extends DateTime> getIntervalSelector() {
return intervalSelector.get();
}
public SimpleObjectProperty<IntervalSelector<? extends DateTime>> intervalSelector() {
return intervalSelector;
}
@Override
public void setIntervalSelector(IntervalSelector<? extends DateTime> newIntervalSelector) {
intervalSelector.set(newIntervalSelector);
}
@Override
public Axis<DateTime> getXAxis() {
return detailsChartDateAxis;
}
@Override
public TimeLineController getController() {
return controller;
}
@Override
public void clearContextMenu() {
setContextMenu(null);
}
public ContextMenu getContextMenu(MouseEvent mouseEvent) throws MissingResourceException {
ContextMenu contextMenu = getContextMenu();
if (contextMenu != null) {
contextMenu.hide();
}
setContextMenu(ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(this, mouseEvent),
ActionUtils.ACTION_SEPARATOR,
TimeLineChart.newZoomHistoyActionGroup(getController())))
);
return getContextMenu();
}
@Override
protected Skin<?> createDefaultSkin() {
return new DetailsChartSkin(this);
}
ObservableList<EventStripe> getRootEventStripes() {
return eventStripes;
}
@NbBundle.Messages({"HideDescriptionAction.displayName=Hide",
"HideDescriptionAction.displayMsg=Hide this group from the details view."})
static class HideDescriptionAction extends Action {
static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS
HideDescriptionAction(String description, DescriptionLoD descriptionLoD, DetailsChart chart) {
super(Bundle.HideDescriptionAction_displayName());
setLongText(Bundle.HideDescriptionAction_displayMsg());
setGraphic(new ImageView(HIDE));
setEventHandler((ActionEvent t) -> {
final DescriptionFilter testFilter = new DescriptionFilter(
descriptionLoD,
description,
DescriptionFilter.FilterMode.EXCLUDE);
DescriptionFilter descriptionFilter = chart.getController().getQuickHideFilters().stream()
.filter(testFilter::equals)
.findFirst().orElseGet(() -> {
testFilter.selectedProperty().addListener(observable -> chart.requestLayout());
chart.getController().getQuickHideFilters().add(testFilter);
return testFilter;
});
descriptionFilter.setSelected(true);
});
}
}
@NbBundle.Messages({"UnhideDescriptionAction.displayName=Unhide"})
static class UnhideDescriptionAction extends Action {
static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS
UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD, DetailsChart chart) {
super(Bundle.UnhideDescriptionAction_displayName());
setGraphic(new ImageView(SHOW));
setEventHandler((ActionEvent t) ->
chart.getController().getQuickHideFilters().stream()
.filter(descriptionFilter -> descriptionFilter.getDescriptionLoD().equals(descriptionLoD)
&& descriptionFilter.getDescription().equals(description))
.forEach(descriptionfilter -> descriptionfilter.setSelected(false))
);
}
}
static private class DetailsChartSkin extends SkinBase<DetailsChart> {
private static final int MIN_PINNED_LANE_HEIGHT = 50;
private final PrimaryDetailsChartLane primaryLane;
private final ScrollingLaneWrapper mainView;
private final PinnedEventsChartLane pinnedLane;
private final ScrollingLaneWrapper pinnedView;
private final MasterDetailPane masterDetailPane;
private final Pane rootPane;
private double dividerPosition = .1;
private IntervalSelector<? extends DateTime> intervalSelector;
@NbBundle.Messages("DetailViewPane.pinnedLaneLabel.text=Pinned Events")
DetailsChartSkin(DetailsChart chart) {
super(chart);
//initialize chart;
primaryLane = new PrimaryDetailsChartLane(chart, getSkinnable().detailsChartDateAxis, getSkinnable().verticalAxis);
mainView = new ScrollingLaneWrapper(primaryLane);
pinnedLane = new PinnedEventsChartLane(chart, getSkinnable().pinnedDateAxis, new EventAxis<>(Bundle.DetailViewPane_pinnedLaneLabel_text()));
pinnedView = new ScrollingLaneWrapper(pinnedLane);
pinnedLane.setMinHeight(MIN_PINNED_LANE_HEIGHT);
pinnedLane.maxVScrollProperty().addListener((Observable observable) -> syncPinnedHeight());
syncPinnedHeight();
masterDetailPane = new MasterDetailPane(Side.TOP, mainView, pinnedView, false);
masterDetailPane.setDividerPosition(dividerPosition);
masterDetailPane.prefHeightProperty().bind(getSkinnable().heightProperty());
masterDetailPane.prefWidthProperty().bind(getSkinnable().widthProperty());
rootPane = new Pane(masterDetailPane);
getChildren().add(rootPane);
//maintain highlighted effect on correct nodes
getSkinnable().highlightPredicate.addListener((observable, oldPredicate, newPredicate) -> {
getAllEventNodes().forEach(eNode ->
eNode.applyHighlightEffect(newPredicate.test(eNode)));
});
TimeLineChart.MouseClickedHandler<DateTime, DetailsChart> mouseClickedHandler = new TimeLineChart.MouseClickedHandler<>(getSkinnable());
TimeLineChart.ChartDragHandler<DateTime, DetailsChart> chartDragHandler = new TimeLineChart.ChartDragHandler<>(getSkinnable());
configureMouseListeners(primaryLane, mouseClickedHandler, chartDragHandler);
configureMouseListeners(pinnedLane, mouseClickedHandler, chartDragHandler);
getSkinnable().getLayoutSettings().pinnedLaneShowing().addListener(observable -> {
boolean selected = getSkinnable().getLayoutSettings().isPinnedLaneShowing();
if (selected == false) {
dividerPosition = masterDetailPane.getDividerPosition();
}
masterDetailPane.setShowDetailNode(selected);
if (selected) {
syncPinnedHeight();
masterDetailPane.setDividerPosition(dividerPosition);
}
});
getSkinnable().intervalSelector().addListener(observable -> {
if (getSkinnable().getIntervalSelector() == null) {
rootPane.getChildren().remove(intervalSelector);
intervalSelector = null;
} else {
rootPane.getChildren().add(getSkinnable().getIntervalSelector());
intervalSelector = getSkinnable().getIntervalSelector();
}
});
getSkinnable().guideLines.addListener((SetChangeListener.Change<? extends GuideLine> change) -> {
if (change.wasRemoved()) {
rootPane.getChildren().remove(change.getElementRemoved());
}
if (change.wasAdded()) {
rootPane.getChildren().add(change.getElementAdded());
}
});
}
private Iterable<EventNodeBase<?>> getAllEventNodes() {
return Iterables.concat(primaryLane.getAllNodes(), pinnedLane.getAllNodes());
}
/**
* get the DateTime along the x-axis that corresponds to the given
* x-coordinate in the coordinate system of this
* {@link PrimaryDetailsChart}
*
* @param x a x-coordinate in the space of this
* {@link PrimaryDetailsChart}
*
* @return the DateTime along the x-axis corresponding to the given x
* value (in the space of this {@link PrimaryDetailsChart}
*/
public DateTime getDateTimeForPosition(double x) {
return getSkinnable().getXAxis().getValueForDisplay(getSkinnable().getXAxis().parentToLocal(x, 0).getX());
}
private void syncPinnedHeight() {
pinnedView.setMinHeight(MIN_PINNED_LANE_HEIGHT);
pinnedView.setMaxHeight(pinnedLane.maxVScrollProperty().get() + 30);
}
}
}

View File

@ -0,0 +1,439 @@
/*
* 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.timeline.ui.detailview;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.common.collect.TreeRangeMap;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.chart.Axis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import org.joda.time.DateTime;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.filters.AbstractFilter;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane;
import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider;
/**
* One "lane" of a the details visualization, contains all the core logic and
* layout code.
*
* NOTE: It was too hard to control the threading of this chart via the
* complicated default listeners. Instead clients should use {@link #addDataItem(javafx.scene.chart.XYChart.Data)
* } and {@link #removeDataItem(javafx.scene.chart.XYChart.Data) } to add and
* remove data.
*/
abstract class DetailsChartLane<Y extends TimeLineEvent> extends XYChart<DateTime, Y> implements ContextMenuProvider {
private static final String STYLE_SHEET = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS
static final int MINIMUM_EVENT_NODE_GAP = 4;
static final int MINIMUM_ROW_HEIGHT = 24;
private final DetailsChart parentChart;
private final TimeLineController controller;
private final DetailsChartLayoutSettings layoutSettings;
private final ObservableList<EventNodeBase<?>> selectedNodes;
private final Map<Y, EventNodeBase<?>> eventMap = new HashMap<>();
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
final ObservableList< EventNodeBase<?>> nodes = FXCollections.observableArrayList();
final ObservableList< EventNodeBase<?>> sortedNodes = nodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis));
private final boolean useQuickHideFilters;
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass
private double descriptionWidth;
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass
private Set<String> activeQuickHidefilters = new HashSet<>();
boolean quickHideFiltersEnabled() {
return useQuickHideFilters;
}
public void clearContextMenu() {
parentChart.clearContextMenu();
}
@Override
public ContextMenu getContextMenu(MouseEvent clickEvent) {
return parentChart.getContextMenu(clickEvent);
}
EventNodeBase<?> createNode(DetailsChartLane<?> chart, TimeLineEvent event) {
if (event.getEventIDs().size() == 1) {
return new SingleEventNode(this, controller.getEventsModel().getEventById(Iterables.getOnlyElement(event.getEventIDs())), null);
} else if (event instanceof SingleEvent) {
return new SingleEventNode(chart, (SingleEvent) event, null);
} else if (event instanceof EventCluster) {
return new EventClusterNode(chart, (EventCluster) event, null);
} else {
return new EventStripeNode(chart, (EventStripe) event, null);
}
}
@Override
synchronized protected void layoutPlotChildren() {
setCursor(Cursor.WAIT);
if (useQuickHideFilters) {
//These don't change during a layout pass and are expensive to compute per node. So we do it once at the start
activeQuickHidefilters = getController().getQuickHideFilters().stream()
.filter(AbstractFilter::isActive)
.map(DescriptionFilter::getDescription)
.collect(Collectors.toSet());
}
//This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start
descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE;
if (layoutSettings.getBandByType()) {
maxY.set(0);
sortedNodes.stream()
.collect(Collectors.groupingBy(EventNodeBase<?>::getEventType)).values()
.forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get())));
} else {
maxY.set(layoutEventBundleNodes(sortedNodes, 0));
}
doAdditionalLayout();
setCursor(null);
}
public TimeLineController getController() {
return controller;
}
public ObservableList<EventNodeBase<?>> getSelectedNodes() {
return selectedNodes;
}
/**
* listener that triggers chart layout pass
*/
final InvalidationListener layoutInvalidationListener = (Observable o) -> {
layoutPlotChildren();
};
public ReadOnlyDoubleProperty maxVScrollProperty() {
return maxY.getReadOnlyProperty();
}
/**
* the maximum y value used so far during the most recent layout pass
*/
private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0);
DetailsChartLane(DetailsChart parentChart, Axis<DateTime> dateAxis, Axis<Y> verticalAxis, boolean useQuickHideFilters) {
super(dateAxis, verticalAxis);
this.parentChart = parentChart;
this.layoutSettings = parentChart.getLayoutSettings();
this.controller = parentChart.getController();
this.selectedNodes = parentChart.getSelectedNodes();
this.useQuickHideFilters = useQuickHideFilters;
//add a dummy series or the chart is never rendered
setData(FXCollections.observableList(Arrays.asList(new Series<DateTime, Y>())));
Tooltip.install(this, AbstractVisualizationPane.getDefaultTooltip());
dateAxis.setAutoRanging(false);
setLegendVisible(false);
setPadding(Insets.EMPTY);
setAlternativeColumnFillVisible(true);
sceneProperty().addListener(observable -> {
Scene scene = getScene();
if (scene != null && scene.getStylesheets().contains(STYLE_SHEET) == false) {
scene.getStylesheets().add(STYLE_SHEET);
}
});
//add listener for events that should trigger layout
layoutSettings.bandByTypeProperty().addListener(layoutInvalidationListener);
layoutSettings.oneEventPerRowProperty().addListener(layoutInvalidationListener);
layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener);
layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener);
layoutSettings.descrVisibilityProperty().addListener(layoutInvalidationListener);
controller.getQuickHideFilters().addListener(layoutInvalidationListener);
//all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly
getPlotChildren().add(nodeGroup);
}
/**
* Layout the nodes in the given list, starting form the given minimum y
* coordinate via the following algorithm:
*
* We start with a list of nodes (each representing an event) sorted by span
* start time of the underlying event
*
* - initialize empty map (maxXatY) from y-ranges to max used x-value
*
* - for each node:
*
* -- size the node based on its children (use this algorithm recursively)
*
* -- get the event's start position from the dateaxis
*
* -- to position node: check if maxXatY is to the left of the left x coord:
* if maxXatY is less than the left x coord, good, put the current node
* here, mark right x coord as maxXatY, go to next node ; if maxXatY is
* greater than the left x coord, increment y position, do check again until
* maxXatY less than left x coord.
*
* @param nodes collection of nodes to layout, sorted by event
* start time
* @param minY the minimum y coordinate to position the nodes
* at.
* @param descriptionWidth the value of the maximum description width to set
* for each node.
*
* @return the maximum y coordinate used by any of the layed out nodes.
*/
public double layoutEventBundleNodes(final Collection<? extends EventNodeBase<?>> nodes, final double minY) {
// map from y-ranges to maximum x
TreeRangeMap<Double, Double> maxXatY = TreeRangeMap.create();
// maximum y values occupied by any of the given nodes, updated as nodes are layed out.
double localMax = minY;
//for each node do a recursive layout to size it and then position it in first available slot
for (EventNodeBase<?> bundleNode : nodes) {
if (useQuickHideFilters && activeQuickHidefilters.contains(bundleNode.getDescription())) {
//if the node hiden is hidden by quick hide filter, hide it and skip layout
bundleNode.setVisible(false);
bundleNode.setManaged(false);
} else {
layoutBundleHelper(bundleNode);
//get computed height and width
double h = bundleNode.getBoundsInLocal().getHeight();
double w = bundleNode.getBoundsInLocal().getWidth();
//get left and right x coords from axis plus computed width
double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation();
double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP;
//initial test position
double yTop = (layoutSettings.getOneEventPerRow())
? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end
: computeYTop(minY, h, maxXatY, xLeft, xRight);
localMax = Math.max(yTop + h, localMax);
//animate node to new position
bundleNode.animateTo(xLeft, yTop);
}
}
return localMax; //return new max
}
@Override
final public void requestChartLayout() {
super.requestChartLayout();
}
double getXForEpochMillis(Long millis) {
DateTime dateTime = new DateTime(millis);
return getXAxis().getDisplayPosition(dateTime);
}
@Deprecated
@Override
protected void dataItemAdded(Series<DateTime, Y> series, int itemIndex, Data<DateTime, Y> item) {
}
@Deprecated
@Override
protected void dataItemRemoved(Data<DateTime, Y> item, Series<DateTime, Y> series) {
}
@Deprecated
@Override
protected void dataItemChanged(Data<DateTime, Y> item) {
}
@Deprecated
@Override
protected void seriesAdded(Series<DateTime, Y> series, int seriesIndex) {
}
@Deprecated
@Override
protected void seriesRemoved(Series<DateTime, Y> series) {
}
/**
* add an event to this chart
*
* @see note in main section of class JavaDoc
*
* @param event
*/
void addEvent(Y event) {
EventNodeBase<?> eventNode = createNode(this, event);
eventMap.put(event, eventNode);
Platform.runLater(() -> {
nodes.add(eventNode);
nodeGroup.getChildren().add(eventNode);
});
}
/**
* remove an event from this chart
*
* @see note in main section of class JavaDoc
*
* @param event
*/
void removeEvent(Y event) {
EventNodeBase<?> removedNode = eventMap.remove(event);
Platform.runLater(() -> {
nodes.remove(removedNode);
nodeGroup.getChildren().removeAll(removedNode);
});
}
/**
* the group that all event nodes are added to. This facilitates scrolling
* by allowing a single translation of this group.
*/
final Group nodeGroup = new Group();
public synchronized void setVScroll(double vScrollValue) {
nodeGroup.setTranslateY(-vScrollValue);
}
/**
* @return all the nodes that pass the given predicate
*/
synchronized Iterable<EventNodeBase<?>> getAllNodes() {
return getNodes((x) -> true);
}
/**
* @return all the nodes that pass the given predicate
*/
synchronized Iterable<EventNodeBase<?>> getNodes(Predicate<EventNodeBase<?>> p) {
//use this recursive function to flatten the tree of nodes into an single stream.
Function<EventNodeBase<?>, Stream<EventNodeBase<?>>> stripeFlattener =
new Function<EventNodeBase<?>, Stream<EventNodeBase<?>>>() {
@Override
public Stream<EventNodeBase<?>> apply(EventNodeBase<?> node) {
return Stream.concat(
Stream.of(node),
node.getSubNodes().stream().flatMap(this::apply));
}
};
return sortedNodes.stream()
.flatMap(stripeFlattener)
.filter(p).collect(Collectors.toList());
}
/**
* Given information about the current layout pass so far and about a
* particular node, compute the y position of that node.
*
*
* @param yMin the smallest (towards the top of the screen) y position to
* consider
* @param h the height of the node we are trying to position
* @param maxXatY a map from y ranges to the max x within that range. NOTE:
* This map will be updated to include the node in question.
* @param xLeft the left x-cord of the node to position
* @param xRight the left x-cord of the node to position
*
* @return the y position for the node in question.
*
*
*/
double computeYTop(double yMin, double h, TreeRangeMap<Double, Double> maxXatY, double xLeft, double xRight) {
double yTop = yMin;
double yBottom = yTop + h;
//until the node is not overlapping any others try moving it down.
boolean overlapping = true;
while (overlapping) {
overlapping = false;
//check each pixel from bottom to top.
for (double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) {
final Double maxX = maxXatY.get(y);
if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) {
//if that pixel is already used
//jump top to this y value and repeat until free slot is found.
overlapping = true;
yTop = y + MINIMUM_EVENT_NODE_GAP;
yBottom = yTop + h;
break;
}
}
}
maxXatY.put(Range.closed(yTop, yBottom), xRight);
return yTop;
}
/**
* Set layout parameters on the given node and layout its children
*
* @param eventNode the Node to layout
*/
void layoutBundleHelper(final EventNodeBase< ?> eventNode) {
//make sure it is shown
eventNode.setVisible(true);
eventNode.setManaged(true);
//apply advanced layout description visibility options
eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility());
eventNode.setMaxDescriptionWidth(descriptionWidth);
//do recursive layout
eventNode.layoutChildren();
}
abstract void doAdditionalLayout();
DetailsChart getParentChart() {
return parentChart;
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.timeline.ui.detailview;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
/**
*
*/
public class DetailsChartLayoutSettings {
/**
* true == truncate all the labels to the greater of the size of their
* timespan indicator or the value of truncateWidth. false == don't truncate
* the labels, alow them to extend past the timespan indicator and off the
* edge of the screen
*/
final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false);
/**
* the width to truncate all labels to if truncateAll is true. adjustable
* via slider if truncateAll is true
*/
final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
/**
* true == layout each event type in its own band, false == mix all the
* events together during layout
*/
private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false);
/**
* true == enforce that no two events can share the same 'row', leading to
* sparser but possibly clearer layout. false == put unrelated events in the
* same 'row', creating a denser more compact layout
*/
private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false);
/**
* how much detail of the description to show in the ui
*/
private final SimpleObjectProperty<DescriptionVisibility> descrVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN);
/**
* is the pinned events lane showing
*/
private final SimpleBooleanProperty pinnedLaneShowing = new SimpleBooleanProperty(false);
public synchronized SimpleBooleanProperty bandByTypeProperty() {
return bandByType;
}
public SimpleBooleanProperty pinnedLaneShowing() {
return pinnedLaneShowing;
}
public boolean isPinnedLaneShowing() {
return pinnedLaneShowing.get();
}
public void setPinnedLaneShowing(boolean showing) {
pinnedLaneShowing.set(showing);
}
SimpleBooleanProperty oneEventPerRowProperty() {
return oneEventPerRow;
}
SimpleDoubleProperty truncateWidthProperty() {
return truncateWidth;
}
SimpleBooleanProperty truncateAllProperty() {
return truncateAll;
}
SimpleObjectProperty< DescriptionVisibility> descrVisibilityProperty() {
return descrVisibility;
}
synchronized void setBandByType(Boolean t1) {
bandByType.set(t1);
}
boolean getBandByType() {
return bandByType.get();
}
boolean getTruncateAll() {
return truncateAll.get();
}
double getTruncateWidth() {
return truncateWidth.get();
}
boolean getOneEventPerRow() {
return oneEventPerRow.get();
}
DescriptionVisibility getDescrVisibility() {
return descrVisibility.get();
}
}

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2014 Basis Technology Corp. * Copyright 2014-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");
@ -22,21 +22,25 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import javafx.scene.chart.Axis; import javafx.scene.chart.Axis;
import javafx.scene.chart.XYChart; import javafx.scene.chart.XYChart;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
/** /**
* No-Op axis that doesn't do anything usefull but is necessary to pass * No-Op axis that doesn't do anything usefull but is necessary to pass
* AggregateEvent as the second member of {@link XYChart.Data} objects * AggregateEvent as the second member of {@link XYChart.Data} objects
*/ */
class EventAxis extends Axis<EventStripe> { class EventAxis<Type extends TimeLineEvent> extends Axis<Type> {
EventAxis(String label) {
setLabel(label);
}
@Override @Override
public double getDisplayPosition(EventStripe value) { public double getDisplayPosition(Type value) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
} }
@Override @Override
public EventStripe getValueForDisplay(double displayPosition) { public Type getValueForDisplay(double displayPosition) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
} }
@ -46,17 +50,17 @@ class EventAxis extends Axis<EventStripe> {
} }
@Override @Override
public boolean isValueOnAxis(EventStripe value) { public boolean isValueOnAxis(Type value) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
} }
@Override @Override
public double toNumericValue(EventStripe value) { public double toNumericValue(Type value) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
} }
@Override @Override
public EventStripe toRealValue(double value) { public Type toRealValue(double value) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
} }
@ -66,7 +70,7 @@ class EventAxis extends Axis<EventStripe> {
} }
@Override @Override
protected List<EventStripe> calculateTickValues(double length, Object range) { protected List<Type> calculateTickValues(double length, Object range) {
return Collections.emptyList(); return Collections.emptyList();
} }
@ -76,7 +80,7 @@ class EventAxis extends Axis<EventStripe> {
} }
@Override @Override
protected String getTickMarkLabel(EventStripe value) { protected String getTickMarkLabel(Type value) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
} }

View File

@ -1,433 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 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.timeline.ui.detailview;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Effect;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.util.Duration;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils;
import org.joda.time.DateTime;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane;
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
/**
*
*/
@NbBundle.Messages({"EventBundleNodeBase.toolTip.loading=loading..."})
public abstract class EventBundleNodeBase<BundleType extends EventBundle<ParentType>, ParentType extends EventBundle<BundleType>, ParentNodeType extends EventBundleNodeBase<ParentType, BundleType, ?>> extends StackPane {
private static final Logger LOGGER = Logger.getLogger(EventBundleNodeBase.class.getName());
private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N NON-NLS
private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N
static final CornerRadii CORNER_RADII_3 = new CornerRadii(3);
static final CornerRadii CORNER_RADII_1 = new CornerRadii(1);
private final Border SELECTION_BORDER;
private static final Map<EventType, Effect> dropShadowMap = new ConcurrentHashMap<>();
static void configureLoDButton(Button b) {
b.setMinSize(16, 16);
b.setMaxSize(16, 16);
b.setPrefSize(16, 16);
show(b, false);
}
static void show(Node b, boolean show) {
b.setVisible(show);
b.setManaged(show);
}
protected final EventDetailsChart chart;
final SimpleObjectProperty<DescriptionLoD> descLOD = new SimpleObjectProperty<>();
final SimpleObjectProperty<DescriptionVisibility> descVisibility = new SimpleObjectProperty<>();
protected final BundleType eventBundle;
protected final ParentNodeType parentNode;
final SleuthkitCase sleuthkitCase;
final FilteredEventsModel eventsModel;
final Background highlightedBackground;
final Background defaultBackground;
final Color evtColor;
final ObservableList<ParentNodeType> subNodes = FXCollections.observableArrayList();
final Pane subNodePane = new Pane();
final Label descrLabel = new Label();
final Label countLabel = new Label();
final ImageView hashIV = new ImageView(HASH_PIN);
final ImageView tagIV = new ImageView(TAG);
final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV);
private final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading());
private Timeline timeline;
public EventBundleNodeBase(EventDetailsChart chart, BundleType eventBundle, ParentNodeType parentNode) {
this.eventBundle = eventBundle;
this.parentNode = parentNode;
this.chart = chart;
this.descLOD.set(eventBundle.getDescriptionLoD());
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
eventsModel = chart.getController().getEventsModel();
evtColor = getEventType().getColor();
defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY));
highlightedBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY));
SELECTION_BORDER = new Border(new BorderStroke(evtColor.darker().desaturate(), BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2)));
if (eventBundle.getEventIDsWithHashHits().isEmpty()) {
show(hashIV, false);
}
if (eventBundle.getEventIDsWithTags().isEmpty()) {
show(tagIV, false);
}
setBackground(defaultBackground);
setAlignment(Pos.TOP_LEFT);
setMaxWidth(USE_PREF_SIZE);
infoHBox.setMaxWidth(USE_PREF_SIZE);
subNodePane.setPrefWidth(USE_COMPUTED_SIZE);
subNodePane.setMinWidth(USE_PREF_SIZE);
subNodePane.setMaxWidth(USE_PREF_SIZE);
/*
* This triggers the layout when a mousover causes the action buttons to
* interesect with another node, forcing it down.
*/
heightProperty().addListener(heightProp -> chart.requestChartLayout());
Platform.runLater(() ->
setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation())
);
//initialize info hbox
infoHBox.setPadding(new Insets(2, 3, 2, 3));
infoHBox.setAlignment(Pos.TOP_LEFT);
Tooltip.install(this, this.tooltip);
//set up mouse hover effect and tooltip
setOnMouseEntered((MouseEvent e) -> {
Tooltip.uninstall(chart, AbstractVisualizationPane.getDefaultTooltip());
showHoverControls(true);
toFront();
});
setOnMouseExited((MouseEvent event) -> {
showHoverControls(false);
if (parentNode != null) {
parentNode.showHoverControls(true);
} else {
Tooltip.install(chart, AbstractVisualizationPane.getDefaultTooltip());
}
});
setOnMouseClicked(new ClickHandler());
descVisibility.addListener(observable -> setDescriptionVisibiltiyImpl(descVisibility.get()));
descVisibility.set(DescriptionVisibility.SHOWN); //trigger listener for initial value
Bindings.bindContent(subNodePane.getChildren(), subNodes);
}
final DescriptionLoD getDescriptionLoD() {
return descLOD.get();
}
public final BundleType getEventBundle() {
return eventBundle;
}
final double getLayoutXCompensation() {
return parentNode != null
? chart.getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis()))
: 0;
}
/**
* install whatever buttons are visible on hover for this node. likes
* tooltips, this had a surprisingly large impact on speed of loading the
* chart
*/
abstract void installActionButtons();
/**
* defer tooltip content creation till needed, this had a surprisingly large
* impact on speed of loading the chart
*/
@NbBundle.Messages({"# {0} - counts",
"# {1} - event type",
"# {2} - description",
"# {3} - start date/time",
"# {4} - end date/time",
"EventBundleNodeBase.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}",
"EventBundleNodeBase.toolTip.loading2=loading tooltip",
"# {0} - hash set count string",
"EventBundleNodeBase.toolTip.hashSetHits=\n\nHash Set Hits\n{0}",
"# {0} - tag count string",
"EventBundleNodeBase.toolTip.tags=\n\nTags\n{0}"})
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void installTooltip() {
if (tooltip.getText().equalsIgnoreCase(Bundle.EventBundleNodeBase_toolTip_loading())) {
final Task<String> tooltTipTask = new Task<String>() {
{
updateTitle(Bundle.EventBundleNodeBase_toolTip_loading2());
}
@Override
protected String call() throws Exception {
HashMap<String, Long> hashSetCounts = new HashMap<>();
if (eventBundle.getEventIDsWithHashHits().isEmpty() == false) {
try {
//TODO:push this to DB
for (TimeLineEvent tle : eventsModel.getEventsById(eventBundle.getEventIDsWithHashHits())) {
Set<String> hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames();
for (String hashSetName : hashSetNames) {
hashSetCounts.merge(hashSetName, 1L, Long::sum);
}
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex); //NON-NLS
}
}
String hashSetCountsString = hashSetCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
Map<String, Long> tagCounts = new HashMap<>();
if (eventBundle.getEventIDsWithTags().isEmpty() == false) {
tagCounts.putAll(eventsModel.getTagCountsByTagName(eventBundle.getEventIDsWithTags()));
}
String tagCountsString = tagCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
return Bundle.EventBundleNodeBase_tooltip_text(getEventIDs().size(), getEventType(), getDescription(),
TimeLineController.getZonedFormatter().print(getStartMillis()),
TimeLineController.getZonedFormatter().print(getEndMillis() + 1000))
+ (hashSetCountsString.isEmpty() ? "" : Bundle.EventBundleNodeBase_toolTip_hashSetHits(hashSetCountsString))
+ (tagCountsString.isEmpty() ? "" : Bundle.EventBundleNodeBase_toolTip_tags(tagCountsString));
}
@Override
protected void succeeded() {
super.succeeded();
try {
tooltip.setText(get());
tooltip.setGraphic(null);
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex); //NON-NLS
}
}
};
new Thread(tooltTipTask).start();
chart.getController().monitorTask(tooltTipTask);
}
}
/**
* apply the 'effect' to visually indicate selection
*
* @param applied true to apply the selection 'effect', false to remove it
*/
public void applySelectionEffect(boolean applied) {
setBorder(applied ? SELECTION_BORDER : null);
}
/**
* apply the 'effect' to visually indicate highlighted nodes
*
* @param applied true to apply the highlight 'effect', false to remove it
*/
abstract void applyHighlightEffect(boolean applied);
@SuppressWarnings("unchecked")
public List<ParentNodeType> getSubNodes() {
return subNodes;
}
abstract void setDescriptionVisibiltiyImpl(DescriptionVisibility get);
void showHoverControls(final boolean showControls) {
Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
eventType -> new DropShadow(-10, eventType.getColor()));
setEffect(showControls ? dropShadow : null);
installTooltip();
enableTooltip(showControls);
if (parentNode != null) {
parentNode.showHoverControls(false);
}
}
final EventType getEventType() {
return getEventBundle().getEventType();
}
final String getDescription() {
return getEventBundle().getDescription();
}
final long getStartMillis() {
return getEventBundle().getStartMillis();
}
final long getEndMillis() {
return getEventBundle().getEndMillis();
}
final Set<Long> getEventIDs() {
return getEventBundle().getEventIDs();
}
@Override
public Orientation getContentBias() {
return Orientation.HORIZONTAL;
}
@Override
protected void layoutChildren() {
chart.layoutEventBundleNodes(subNodes, 0);
super.layoutChildren();
}
abstract ParentNodeType createChildNode(ParentType rawChild);
/**
* @param w the maximum width the description label should have
*/
abstract void setMaxDescriptionWidth(double w);
void setDescriptionVisibility(DescriptionVisibility get) {
descVisibility.set(get);
}
void enableTooltip(boolean toolTipEnabled) {
if (toolTipEnabled) {
Tooltip.install(this, tooltip);
} else {
Tooltip.uninstall(this, tooltip);
}
}
void animateTo(double xLeft, double yTop) {
if (timeline != null) {
timeline.stop();
Platform.runLater(chart::requestChartLayout);
}
timeline = new Timeline(new KeyFrame(Duration.millis(100),
new KeyValue(layoutXProperty(), xLeft),
new KeyValue(layoutYProperty(), yTop))
);
timeline.setOnFinished(finished -> Platform.runLater(chart::requestChartLayout));
timeline.play();
}
abstract EventHandler<MouseEvent> getDoubleClickHandler();
abstract Collection<? extends Action> getActions();
/**
* event handler used for mouse events on {@link EventStripeNode}s
*/
private class ClickHandler implements EventHandler<MouseEvent> {
private ContextMenu contextMenu;
@Override
public void handle(MouseEvent t) {
if (t.getButton() == MouseButton.PRIMARY) {
if (t.getClickCount() > 1) {
getDoubleClickHandler().handle(t);
} else if (t.isShiftDown()) {
chart.selectedNodes.add(EventBundleNodeBase.this);
} else if (t.isShortcutDown()) {
chart.selectedNodes.removeAll(EventBundleNodeBase.this);
} else {
chart.selectedNodes.setAll(EventBundleNodeBase.this);
}
t.consume();
} else if (t.getButton() == MouseButton.SECONDARY) {
ContextMenu chartContextMenu = chart.getChartContextMenu(t);
if (contextMenu == null) {
contextMenu = new ContextMenu();
contextMenu.setAutoHide(true);
contextMenu.getItems().addAll(ActionUtils.createContextMenu(getActions()).getItems());
contextMenu.getItems().add(new SeparatorMenuItem());
contextMenu.getItems().addAll(chartContextMenu.getItems());
}
contextMenu.show(EventBundleNodeBase.this, t.getScreenX(), t.getScreenY());
t.consume();
}
}
}
}

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-15 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");
@ -18,16 +18,15 @@
*/ */
package org.sleuthkit.autopsy.timeline.ui.detailview; package org.sleuthkit.autopsy.timeline.ui.detailview;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -51,11 +50,11 @@ import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.RootFilter;
import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton; import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configureActionButton;
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
@ -63,32 +62,32 @@ import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
/** /**
* *
*/ */
final public class EventClusterNode extends EventBundleNodeBase<EventCluster, EventStripe, EventStripeNode> { final public class EventClusterNode extends MultiEventNodeBase<EventCluster, EventStripe, EventStripeNode> {
private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1); private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1);
private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N
private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N
private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS));
private Button plusButton; Button plusButton;
private Button minusButton; Button minusButton;
@Override @Override
void installActionButtons() { void installActionButtons() {
super.installActionButtons();
if (plusButton == null) { if (plusButton == null) {
plusButton = ActionUtils.createButton(new ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE); plusButton = ActionUtils.createButton(new ExpandClusterAction(this), ActionUtils.ActionTextBehavior.HIDE);
minusButton = ActionUtils.createButton(new CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE); minusButton = ActionUtils.createButton(new CollapseClusterAction(this), ActionUtils.ActionTextBehavior.HIDE);
controlsHBox.getChildren().addAll(minusButton, plusButton);
configureLoDButton(plusButton); configureActionButton(plusButton);
configureLoDButton(minusButton); configureActionButton(minusButton);
infoHBox.getChildren().addAll(minusButton, plusButton);
} }
} }
public EventClusterNode(EventDetailsChart chart, EventCluster eventCluster, EventStripeNode parentNode) { EventClusterNode(DetailsChartLane<?> chartLane, EventCluster eventCluster, EventStripeNode parentNode) {
super(chart, eventCluster, parentNode); super(chartLane, eventCluster, parentNode);
subNodePane.setBorder(clusterBorder); subNodePane.setBorder(clusterBorder);
subNodePane.setBackground(defaultBackground); subNodePane.setBackground(defaultBackground);
@ -98,45 +97,19 @@ final public class EventClusterNode extends EventBundleNodeBase<EventCluster, Ev
setAlignment(Pos.CENTER_LEFT); setAlignment(Pos.CENTER_LEFT);
setCursor(Cursor.HAND); setCursor(Cursor.HAND);
getChildren().addAll(subNodePane, infoHBox); getChildren().addAll(subNodePane, infoHBox);
if (parentNode == null) {
setDescriptionVisibility(DescriptionVisibility.SHOWN);
}
} }
@Override @Override
void showHoverControls(final boolean showControls) { void showFullDescription(final int size) {
super.showHoverControls(showControls); if (getParentNode().isPresent()) {
installActionButtons(); showCountOnly(size);
show(plusButton, showControls); } else {
show(minusButton, showControls); super.showFullDescription(size);
}
@Override
void applyHighlightEffect(boolean applied) {
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
void setMaxDescriptionWidth(double max) {
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
void setDescriptionVisibiltiyImpl(DescriptionVisibility descrVis) {
final int size = getEventCluster().getCount();
switch (descrVis) {
case HIDDEN:
countLabel.setText("");
descrLabel.setText("");
break;
case COUNT_ONLY:
descrLabel.setText("");
countLabel.setText(String.valueOf(size));
break;
default:
case SHOWN:
countLabel.setText(String.valueOf(size));
break;
} }
} }
@ -146,11 +119,10 @@ final public class EventClusterNode extends EventBundleNodeBase<EventCluster, Ev
* @param requestedDescrLoD * @param requestedDescrLoD
* @param expand * @param expand
*/ */
@NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters") @NbBundle.Messages(value = "EventClusterNode.loggedTask.name=Load sub events")
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private synchronized void loadSubBundles(DescriptionLoD.RelativeDetail relativeDetail) { private synchronized void loadSubStripes(DescriptionLoD.RelativeDetail relativeDetail) {
chart.setCursor(Cursor.WAIT); getChartLane().setCursor(Cursor.WAIT);
/* /*
* make new ZoomParams to query with * make new ZoomParams to query with
@ -164,74 +136,75 @@ final public class EventClusterNode extends EventBundleNodeBase<EventCluster, Ev
final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get();
final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD());
Task<List<EventStripe>> loggedTask = new LoggedTask<List<EventStripe>>(Bundle.EventStripeNode_loggedTask_name(), false) { Task<List<EventStripe>> loggedTask = new LoggedTask<List<EventStripe>>(Bundle.EventClusterNode_loggedTask_name(), false) {
private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail);
@Override @Override
protected List<EventStripe> call() throws Exception { protected List<EventStripe> call() throws Exception {
List<EventStripe> bundles; List<EventStripe> stripes;
DescriptionLoD next = loadedDescriptionLoD; DescriptionLoD next = loadedDescriptionLoD;
do { do {
loadedDescriptionLoD = next; loadedDescriptionLoD = next;
if (loadedDescriptionLoD == getEventBundle().getDescriptionLoD()) { if (loadedDescriptionLoD == getEvent().getDescriptionLoD()) {
return Collections.emptyList(); return Collections.emptyList();
} }
bundles = eventsModel.getEventStripes(zoomParams.withDescrLOD(loadedDescriptionLoD)); stripes = eventsModel.getEventStripes(zoomParams.withDescrLOD(loadedDescriptionLoD));
next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); next = loadedDescriptionLoD.withRelativeDetail(relativeDetail);
} while (bundles.size() == 1 && nonNull(next)); } while (stripes.size() == 1 && nonNull(next));
// return list of EventStripes representing sub-bundles // return list of EventStripes representing sub-bundles
return bundles.stream() return stripes.stream()
.map(eventStripe -> eventStripe.withParent(getEventCluster())) .map(eventStripe -> eventStripe.withParent(getEvent()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override @Override
protected void succeeded() { protected void succeeded() {
try { try {
List<EventStripe> bundles = get(); List<EventStripe> newSubStripes = get();
//clear the existing subnodes //clear the existing subnodes
List<EventStripe> transform = subNodes.stream().flatMap(new StripeFlattener()).collect(Collectors.toList()); List<TimeLineEvent> oldSubStripes = subNodes.stream().flatMap(new StripeFlattener()).collect(Collectors.toList());
chart.getEventStripes().removeAll(transform); getChartLane().getParentChart().getAllNestedEventStripes().removeAll(oldSubStripes);
subNodes.clear(); subNodes.clear();
if (bundles.isEmpty()) { if (newSubStripes.isEmpty()) {
getChildren().setAll(subNodePane, infoHBox); getChildren().setAll(subNodePane, infoHBox);
descLOD.set(getEventBundle().getDescriptionLoD()); setDescriptionLOD(getEvent().getDescriptionLoD());
} else { } else {
chart.getEventStripes().addAll(bundles); getChartLane().getParentChart().getAllNestedEventStripes().addAll(newSubStripes);
subNodes.addAll(Lists.transform(bundles, EventClusterNode.this::createChildNode)); subNodes.addAll(Lists.transform(newSubStripes, EventClusterNode.this::createChildNode));
getChildren().setAll(new VBox(infoHBox, subNodePane)); getChildren().setAll(new VBox(infoHBox, subNodePane));
descLOD.set(loadedDescriptionLoD); setDescriptionLOD(loadedDescriptionLoD);
} }
} catch (InterruptedException | ExecutionException ex) { } catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); //NON-NLS LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); //NON-NLS
} }
chart.requestChartLayout(); getChartLane().requestChartLayout();
chart.setCursor(null); getChartLane().setCursor(null);
} }
}; };
new Thread(loggedTask).start(); new Thread(loggedTask).start();
//start task //start task
chart.getController().monitorTask(loggedTask); getChartLane().getController().monitorTask(loggedTask);
} }
@Override @Override
EventStripeNode createChildNode(EventStripe stripe) { EventNodeBase<?> createChildNode(EventStripe stripe) {
return new EventStripeNode(chart, stripe, this); if (stripe.getEventIDs().size() == 1) {
return new SingleEventNode(getChartLane(), getChartLane().getController().getEventsModel().getEventById(Iterables.getOnlyElement(stripe.getEventIDs())), this);
} else {
return new EventStripeNode(getChartLane(), stripe, this);
} }
EventCluster getEventCluster() {
return getEventBundle();
} }
@Override @Override
protected void layoutChildren() { protected void layoutChildren() {
double chartX = chart.getXAxis().getDisplayPosition(new DateTime(getStartMillis())); double chartX = getChartLane().getXAxis().getDisplayPosition(new DateTime(getStartMillis()));
double w = chart.getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX; double w = getChartLane().getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX;
subNodePane.setPrefWidth(Math.max(1, w)); subNodePane.setPrefWidth(Math.max(1, w));
super.layoutChildren(); super.layoutChildren();
} }
@ -244,52 +217,58 @@ final public class EventClusterNode extends EventBundleNodeBase<EventCluster, Ev
RootFilter getSubClusterFilter() { RootFilter getSubClusterFilter() {
RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf(); RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf();
subClusterFilter.getSubFilters().addAll( subClusterFilter.getSubFilters().addAll(
new DescriptionFilter(getEventBundle().getDescriptionLoD(), getDescription(), DescriptionFilter.FilterMode.INCLUDE), new DescriptionFilter(getEvent().getDescriptionLoD(), getDescription(), DescriptionFilter.FilterMode.INCLUDE),
new TypeFilter(getEventType())); new TypeFilter(getEventType()));
return subClusterFilter; return subClusterFilter;
} }
@Override @Override
Collection<? extends Action> getActions() { Iterable<? extends Action> getActions() {
return Arrays.asList(new ExpandClusterAction(), return Iterables.concat(
new CollapseClusterAction()); super.getActions(),
Arrays.asList(new ExpandClusterAction(this), new CollapseClusterAction(this))
);
} }
@Override @Override
EventHandler<MouseEvent> getDoubleClickHandler() { EventHandler<MouseEvent> getDoubleClickHandler() {
return mouseEvent -> new ExpandClusterAction().handle(null); return mouseEvent -> new ExpandClusterAction(this).handle(null);
} }
private class ExpandClusterAction extends Action { static private class ExpandClusterAction extends Action {
@NbBundle.Messages(value = "ExpandClusterAction.text=Expand") private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N
ExpandClusterAction() {
@NbBundle.Messages({"ExpandClusterAction.text=Expand"})
ExpandClusterAction(EventClusterNode node) {
super(Bundle.ExpandClusterAction_text()); super(Bundle.ExpandClusterAction_text());
setGraphic(new ImageView(PLUS)); setGraphic(new ImageView(PLUS));
setEventHandler(actionEvent -> { setEventHandler(actionEvent -> {
if (descLOD.get().moreDetailed() != null) { if (node.getDescriptionLoD().moreDetailed() != null) {
loadSubBundles(DescriptionLoD.RelativeDetail.MORE); node.loadSubStripes(DescriptionLoD.RelativeDetail.MORE);
} }
}); });
disabledProperty().bind(descLOD.isEqualTo(DescriptionLoD.FULL)); disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(DescriptionLoD.FULL));
} }
} }
private class CollapseClusterAction extends Action { static private class CollapseClusterAction extends Action {
@NbBundle.Messages(value = "CollapseClusterAction.text=Collapse") private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N
CollapseClusterAction() {
@NbBundle.Messages({"CollapseClusterAction.text=Collapse"})
CollapseClusterAction(EventClusterNode node) {
super(Bundle.CollapseClusterAction_text()); super(Bundle.CollapseClusterAction_text());
setGraphic(new ImageView(MINUS)); setGraphic(new ImageView(MINUS));
setEventHandler(actionEvent -> { setEventHandler(actionEvent -> {
if (descLOD.get().lessDetailed() != null) { if (node.getDescriptionLoD().lessDetailed() != null) {
loadSubBundles(DescriptionLoD.RelativeDetail.LESS); node.loadSubStripes(DescriptionLoD.RelativeDetail.LESS);
} }
}); });
disabledProperty().bind(Bindings.createBooleanBinding(() -> nonNull(getEventCluster()) && descLOD.get() == getEventCluster().getDescriptionLoD(), descLOD));
}
}
disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(node.getEvent().getDescriptionLoD()));
}
}
} }

View File

@ -1,768 +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.timeline.ui.detailview;
import com.google.common.collect.Range;
import com.google.common.collect.TreeRangeMap;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.chart.Axis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Line;
import javafx.scene.shape.StrokeLineCap;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.filters.AbstractFilter;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane;
import org.sleuthkit.autopsy.timeline.ui.IntervalSelector;
import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
/**
* Custom implementation of {@link XYChart} to graph events on a horizontal
* timeline.
*
* The horizontal {@link DateAxis} controls the tick-marks and the horizontal
* layout of the nodes representing events. The vertical {@link NumberAxis} does
* nothing (although a custom implementation could help with the vertical
* layout?)
*
* Series help organize events for the banding by event type, we could add a
* node to contain each band if we need a place for per band controls.
*
* NOTE: It was too hard to control the threading of this chart via the
* complicated default listeners. Instead clients should use null {@link #addDataItem(javafx.scene.chart.XYChart.Data)
* } and {@link #removeDataItem(javafx.scene.chart.XYChart.Data) } to add and
* remove data.
*
* //TODO: refactor the projected lines to a separate class. -jm
*/
public final class EventDetailsChart extends XYChart<DateTime, EventStripe> implements TimeLineChart<DateTime> {
private static final String styleSheet = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS
private static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS
private static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS
private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); //NON-NLS
private static final int PROJECTED_LINE_Y_OFFSET = 5;
private static final int PROJECTED_LINE_STROKE_WIDTH = 5;
private static final int MINIMUM_EVENT_NODE_GAP = 4;
private final static int MINIMUM_ROW_HEIGHT = 24;
private final TimeLineController controller;
private final FilteredEventsModel filteredEvents;
private ContextMenu chartContextMenu;
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass
private Set<String> activeQuickHidefilters;
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass
private double descriptionWidth;
@Override
public ContextMenu getChartContextMenu() {
return chartContextMenu;
}
/**
* a user positionable vertical line to help compare events
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private Line guideLine;
/**
* * the user can drag out a time range to zoom into and this
* {@link IntervalSelector} is the visual representation of it while the
* user is dragging
*/
private IntervalSelector<? extends DateTime> intervalSelector;
/**
* listener that triggers chart layout pass
*/
private final InvalidationListener layoutInvalidationListener = (Observable o) -> {
layoutPlotChildren();
};
/**
* the maximum y value used so far during the most recent layout pass
*/
private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0);
final ObservableList<EventBundleNodeBase<?, ?, ?>> selectedNodes;
/**
* the group that all event nodes are added to. This facilitates scrolling
* by allowing a single translation of this group.
*/
private final Group nodeGroup = new Group();
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final ObservableList<EventStripe> eventStripes = FXCollections.observableArrayList();
private final ObservableList< EventStripeNode> stripeNodes = FXCollections.observableArrayList();
private final ObservableList< EventStripeNode> sortedStripeNodes = stripeNodes.sorted(Comparator.comparing(EventStripeNode::getStartMillis));
private final Map<EventCluster, Line> projectionMap = new ConcurrentHashMap<>();
/**
* true == layout each event type in its own band, false == mix all the
* events together during layout
*/
private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false);
/**
* true == enforce that no two events can share the same 'row', leading to
* sparser but possibly clearer layout. false == put unrelated events in the
* same 'row', creating a denser more compact layout
*/
private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false);
/**
* how much detail of the description to show in the ui
*/
private final SimpleObjectProperty<DescriptionVisibility> descrVisibility =
new SimpleObjectProperty<>(DescriptionVisibility.SHOWN);
/**
* true == truncate all the labels to the greater of the size of their
* timespan indicator or the value of truncateWidth. false == don't truncate
* the labels, alow them to extend past the timespan indicator and off the
* edge of the screen
*/
final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false);
/**
* the width to truncate all labels to if truncateAll is true. adjustable
* via slider if truncateAll is true
*/
final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
EventDetailsChart(TimeLineController controller, DateAxis dateAxis, final Axis<EventStripe> verticalAxis, ObservableList<EventBundleNodeBase<?, ?, ?>> selectedNodes) {
super(dateAxis, verticalAxis);
this.controller = controller;
this.filteredEvents = this.controller.getEventsModel();
sceneProperty().addListener(observable -> {
Scene scene = getScene();
if (scene != null && scene.getStylesheets().contains(styleSheet) == false) {
scene.getStylesheets().add(styleSheet);
}
});
filteredEvents.zoomParametersProperty().addListener(o -> {
clearGuideLine();
clearIntervalSelector();
selectedNodes.clear();
projectionMap.clear();
controller.selectEventIDs(Collections.emptyList());
});
Tooltip.install(this, AbstractVisualizationPane.getDefaultTooltip());
dateAxis.setAutoRanging(false);
verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm
verticalAxis.setTickLabelsVisible(false);
verticalAxis.setTickMarkVisible(false);
setLegendVisible(false);
setPadding(Insets.EMPTY);
setAlternativeColumnFillVisible(true);
//all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly
getPlotChildren().add(nodeGroup);
//add listener for events that should trigger layout
bandByType.addListener(layoutInvalidationListener);
oneEventPerRow.addListener(layoutInvalidationListener);
truncateAll.addListener(layoutInvalidationListener);
truncateWidth.addListener(layoutInvalidationListener);
descrVisibility.addListener(layoutInvalidationListener);
getController().getQuickHideFilters().addListener(layoutInvalidationListener);
//this is needed to allow non circular binding of the guideline and timerangeRect heights to the height of the chart
//TODO: seems like a hack, can we remove? -jm
boundsInLocalProperty().addListener((Observable observable) -> {
setPrefHeight(boundsInLocalProperty().get().getHeight());
});
ChartDragHandler<DateTime, EventDetailsChart> chartDragHandler = new ChartDragHandler<>(this);
setOnMousePressed(chartDragHandler);
setOnMouseReleased(chartDragHandler);
setOnMouseDragged(chartDragHandler);
setOnMouseClicked(new MouseClickedHandler<>(this));
this.selectedNodes = selectedNodes;
this.selectedNodes.addListener(new SelectionChangeHandler());
}
ObservableList<EventStripe> getEventStripes() {
return eventStripes;
}
@Override
public TimeLineController getController() {
return controller;
}
@Override
public ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException {
if (chartContextMenu != null) {
chartContextMenu.hide();
}
chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(clickEvent),
TimeLineChart.newZoomHistoyActionGroup(controller)));
chartContextMenu.setAutoHide(true);
return chartContextMenu;
}
@Override
public void clearIntervalSelector() {
getChartChildren().remove(intervalSelector);
intervalSelector = null;
}
public synchronized SimpleBooleanProperty bandByTypeProperty() {
return bandByType;
}
@Override
public IntervalSelector<DateTime> newIntervalSelector() {
return new DetailIntervalSelector(this);
}
synchronized void setBandByType(Boolean t1) {
bandByType.set(t1);
}
/**
* get the DateTime along the x-axis that corresponds to the given
* x-coordinate in the coordinate system of this {@link EventDetailsChart}
*
* @param x a x-coordinate in the space of this {@link EventDetailsChart}
*
* @return the DateTime along the x-axis corresponding to the given x value
* (in the space of this {@link EventDetailsChart}
*/
public DateTime getDateTimeForPosition(double x) {
return getXAxis().getValueForDisplay(getXAxis().parentToLocal(x, 0).getX());
}
@Override
public IntervalSelector<? extends DateTime> getIntervalSelector() {
return intervalSelector;
}
@Override
public void setIntervalSelector(IntervalSelector<? extends DateTime> newIntervalSelector) {
intervalSelector = newIntervalSelector;
getChartChildren().add(getIntervalSelector());
}
SimpleBooleanProperty oneEventPerRowProperty() {
return oneEventPerRow;
}
SimpleDoubleProperty getTruncateWidth() {
return truncateWidth;
}
SimpleBooleanProperty truncateAllProperty() {
return truncateAll;
}
SimpleObjectProperty< DescriptionVisibility> descrVisibilityProperty() {
return descrVisibility;
}
/**
* @see note in main section of class JavaDoc
*
* @param series
* @param i
*/
@Override
protected void seriesAdded(Series<DateTime, EventStripe> series, int i) {
}
/**
* @see note in main section of class JavaDoc
*
* @param series
*/
@Override
protected void seriesRemoved(Series<DateTime, EventStripe> series) {
}
/**
* @see note in main section of class JavaDoc
*
* @param series
* @param itemIndex
* @param item
*/
@Override
protected void dataItemAdded(Series<DateTime, EventStripe> series, int itemIndex, Data<DateTime, EventStripe> item) {
}
/**
* @see note in main section of class JavaDoc
*
*
* @param item
* @param series
*/
@Override
protected void dataItemRemoved(Data<DateTime, EventStripe> item, Series<DateTime, EventStripe> series) {
}
/**
* @see note in main section of class JavaDoc
*
* @param item
*/
@Override
protected void dataItemChanged(Data<DateTime, EventStripe> item) {
}
/**
* add a dataitem to this chart
*
* @see note in main section of class JavaDoc
*
* @param data
*/
void addDataItem(Data<DateTime, EventStripe> data) {
final EventStripe eventStripe = data.getYValue();
EventStripeNode stripeNode = new EventStripeNode(EventDetailsChart.this, eventStripe, null);
Platform.runLater(() -> {
eventStripes.add(eventStripe);
stripeNodes.add(stripeNode);
nodeGroup.getChildren().add(stripeNode);
data.setNode(stripeNode);
});
}
/**
* remove a data item from this chart
*
* @see note in main section of class JavaDoc
*
* @param data
*/
void removeDataItem(Data<DateTime, EventStripe> data) {
Platform.runLater(() -> {
EventStripeNode removedNode = (EventStripeNode) data.getNode();
eventStripes.removeAll(new StripeFlattener().apply(removedNode).collect(Collectors.toList()));
stripeNodes.removeAll(removedNode);
nodeGroup.getChildren().removeAll(removedNode);
data.setNode(null);
});
}
@Override
protected void layoutPlotChildren() {
setCursor(Cursor.WAIT);
maxY.set(0);
//These don't change during a layout pass and are expensive to compute per node. So we do it once at the start
activeQuickHidefilters = getController().getQuickHideFilters().stream()
.filter(AbstractFilter::isActive)
.map(DescriptionFilter::getDescription)
.collect(Collectors.toSet());
//This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start
descriptionWidth = truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE;
if (bandByType.get()) {
sortedStripeNodes.stream()
.collect(Collectors.groupingBy(EventStripeNode::getEventType)).values()
.forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get())));
} else {
maxY.set(layoutEventBundleNodes(sortedStripeNodes.sorted(Comparator.comparing(EventStripeNode::getStartMillis)), 0));
}
layoutProjectionMap();
setCursor(null);
}
ReadOnlyDoubleProperty maxVScrollProperty() {
return maxY.getReadOnlyProperty();
}
/**
* @return all the nodes that pass the given predicate
*/
synchronized Iterable<EventBundleNodeBase<?, ?, ?>> getNodes(Predicate<EventBundleNodeBase<?, ?, ?>> p) {
//use this recursive function to flatten the tree of nodes into an single stream.
Function<EventBundleNodeBase<?, ?, ?>, Stream<EventBundleNodeBase<?, ?, ?>>> stripeFlattener =
new Function<EventBundleNodeBase<?, ?, ?>, Stream<EventBundleNodeBase<?, ?, ?>>>() {
@Override
public Stream<EventBundleNodeBase<?, ?, ?>> apply(EventBundleNodeBase<?, ?, ?> node) {
return Stream.concat(
Stream.of(node),
node.getSubNodes().stream().flatMap(this::apply));
}
};
return sortedStripeNodes.stream()
.flatMap(stripeFlattener)
.filter(p).collect(Collectors.toList());
}
synchronized void setVScroll(double vScrollValue) {
nodeGroup.setTranslateY(-vScrollValue);
}
void clearGuideLine() {
getChartChildren().remove(guideLine);
guideLine = null;
}
/**
* Layout the nodes in the given list, starting form the given minimum y
* coordinate via the following algorithm:
*
* We start with a list of nodes (each representing an event) sorted by span
* start time of the underlying event
*
* - initialize empty map (maxXatY) from y-ranges to max used x-value
*
* - for each node:
*
* -- size the node based on its children (use this algorithm recursively)
*
* -- get the event's start position from the dateaxis
*
* -- to position node: check if maxXatY is to the left of the left x coord:
* if maxXatY is less than the left x coord, good, put the current node
* here, mark right x coord as maxXatY, go to next node ; if maxXatY is
* greater than the left x coord, increment y position, do check again until
* maxXatY less than left x coord.
*
* @param nodes collection of nodes to layout, sorted by event
* start time
* @param minY the minimum y coordinate to position the nodes
* at.
* @param descriptionWidth the value of the maximum description width to set
* for each node.
*
* @return the maximum y coordinate used by any of the layed out nodes.
*/
double layoutEventBundleNodes(final Collection<? extends EventBundleNodeBase<?, ?, ?>> nodes, final double minY) {
// map from y-ranges to maximum x
TreeRangeMap<Double, Double> maxXatY = TreeRangeMap.create();
// maximum y values occupied by any of the given nodes, updated as nodes are layed out.
double localMax = minY;
//for each node do a recursive layout to size it and then position it in first available slot
for (EventBundleNodeBase<?, ?, ?> bundleNode : nodes) {
//is the node hiden by a quick hide filter?
boolean quickHide = activeQuickHidefilters.contains(bundleNode.getDescription());
if (quickHide) {
//hide it and skip layout
bundleNode.setVisible(false);
bundleNode.setManaged(false);
} else {
layoutBundleHelper(bundleNode);
//get computed height and width
double h = bundleNode.getBoundsInLocal().getHeight();
double w = bundleNode.getBoundsInLocal().getWidth();
//get left and right x coords from axis plus computed width
double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation();
double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP;
//initial test position
double yTop = (oneEventPerRow.get())
? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end
: computeYTop(minY, h, maxXatY, xLeft, xRight);
localMax = Math.max(yTop + h, localMax);
if ((xLeft != bundleNode.getLayoutX()) || (yTop != bundleNode.getLayoutY())) {
//animate node to new position
bundleNode.animateTo(xLeft, yTop);
}
}
}
return localMax; //return new max
}
/**
* Given information about the current layout pass so far and about a
* particular node, compute the y position of that node.
*
*
* @param yMin the smallest (towards the top of the screen) y position to
* consider
* @param h the height of the node we are trying to position
* @param maxXatY a map from y ranges to the max x within that range. NOTE:
* This map will be updated to include the node in question.
* @param xLeft the left x-cord of the node to position
* @param xRight the left x-cord of the node to position
*
* @return the y position for the node in question.
*
*
*/
private double computeYTop(double yMin, double h, TreeRangeMap<Double, Double> maxXatY, double xLeft, double xRight) {
double yTop = yMin;
double yBottom = yTop + h;
//until the node is not overlapping any others try moving it down.
boolean overlapping = true;
while (overlapping) {
overlapping = false;
//check each pixel from bottom to top.
for (double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) {
final Double maxX = maxXatY.get(y);
if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) {
//if that pixel is already used
//jump top to this y value and repeat until free slot is found.
overlapping = true;
yTop = y + MINIMUM_EVENT_NODE_GAP;
yBottom = yTop + h;
break;
}
}
}
maxXatY.put(Range.closed(yTop, yBottom), xRight);
return yTop;
}
/**
*
* Set layout paramaters on the given node and layout its children
*
* @param bundleNode the Node to layout
* @param descriptionWdith the maximum width for the description text
*/
private void layoutBundleHelper(final EventBundleNodeBase<?, ?, ?> bundleNode) {
//make sure it is shown
bundleNode.setVisible(true);
bundleNode.setManaged(true);
//apply advanced layout description visibility options
bundleNode.setDescriptionVisibility(descrVisibility.get());
bundleNode.setMaxDescriptionWidth(descriptionWidth);
//do recursive layout
bundleNode.layoutChildren();
}
/**
* expose as protected
*/
@Override
protected void requestChartLayout() {
super.requestChartLayout();
}
private double getXForEpochMillis(Long millis) {
DateTime dateTime = new DateTime(millis);
return getXAxis().getDisplayPosition(dateTime);
}
private double getParentXForEpochMillis(Long epochMillis) {
return getXAxis().localToParent(getXForEpochMillis(epochMillis), 0).getX();
}
private void layoutProjectionMap() {
for (final Map.Entry<EventCluster, Line> entry : projectionMap.entrySet()) {
final EventCluster cluster = entry.getKey();
final Line line = entry.getValue();
line.setStartX(getParentXForEpochMillis(cluster.getStartMillis()));
line.setEndX(getParentXForEpochMillis(cluster.getEndMillis()));
line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
}
}
/**
* @return the filteredEvents
*/
public FilteredEventsModel getFilteredEvents() {
return filteredEvents;
}
static private class DetailIntervalSelector extends IntervalSelector<DateTime> {
DetailIntervalSelector(EventDetailsChart chart) {
super(chart);
}
@Override
protected String formatSpan(DateTime date) {
return date.toString(TimeLineController.getZonedFormatter());
}
@Override
protected Interval adjustInterval(Interval i) {
return i;
}
@Override
protected DateTime parseDateTime(DateTime date) {
return date;
}
}
private class PlaceMarkerAction extends Action {
@NbBundle.Messages({"EventDetailChart.chartContextMenu.placeMarker.name=Place Marker"})
PlaceMarkerAction(MouseEvent clickEvent) {
super(Bundle.EventDetailChart_chartContextMenu_placeMarker_name());
setGraphic(new ImageView(MARKER)); // NON-NLS
setEventHandler(actionEvent -> {
if (guideLine == null) {
guideLine = new GuideLine(EventDetailsChart.this);
guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0);
getChartChildren().add(guideLine);
} else {
guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0);
}
});
}
}
private class SelectionChangeHandler implements ListChangeListener<EventBundleNodeBase<?, ?, ?>> {
private final Axis<DateTime> dateAxis;
SelectionChangeHandler() {
dateAxis = getXAxis();
}
@Override
public void onChanged(ListChangeListener.Change<? extends EventBundleNodeBase<?, ?, ?>> change) {
while (change.next()) {
change.getRemoved().forEach((EventBundleNodeBase<?, ?, ?> removedNode) -> {
removedNode.getEventBundle().getClusters().forEach(cluster -> {
Line removedLine = projectionMap.remove(cluster);
getChartChildren().removeAll(removedLine);
});
});
change.getAddedSubList().forEach((EventBundleNodeBase<?, ?, ?> addedNode) -> {
for (EventCluster range : addedNode.getEventBundle().getClusters()) {
Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
);
line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5));
line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
line.setStrokeLineCap(StrokeLineCap.ROUND);
projectionMap.put(range, line);
getChartChildren().add(line);
}
});
}
EventDetailsChart.this.controller.selectEventIDs(selectedNodes.stream()
.flatMap(detailNode -> detailNode.getEventIDs().stream())
.collect(Collectors.toList()));
}
}
@NbBundle.Messages({"HideDescriptionAction.displayName=Hide",
"HideDescriptionAction.displayMsg=Hide this group from the details view."})
class HideDescriptionAction extends Action {
HideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
super(Bundle.HideDescriptionAction_displayName());
setLongText(Bundle.HideDescriptionAction_displayMsg());
setGraphic(new ImageView(HIDE));
setEventHandler((ActionEvent t) -> {
final DescriptionFilter testFilter = new DescriptionFilter(
descriptionLoD,
description,
DescriptionFilter.FilterMode.EXCLUDE);
DescriptionFilter descriptionFilter = getController().getQuickHideFilters().stream()
.filter(testFilter::equals)
.findFirst().orElseGet(() -> {
testFilter.selectedProperty().addListener(observable -> requestChartLayout());
getController().getQuickHideFilters().add(testFilter);
return testFilter;
});
descriptionFilter.setSelected(true);
});
}
}
@NbBundle.Messages({"UnhideDescriptionAction.displayName=Unhide"})
class UnhideDescriptionAction extends Action {
UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
super(Bundle.UnhideDescriptionAction_displayName());
setGraphic(new ImageView(SHOW));
setEventHandler((ActionEvent t) ->
getController().getQuickHideFilters().stream()
.filter(descriptionFilter -> descriptionFilter.getDescriptionLoD().equals(descriptionLoD)
&& descriptionFilter.getDescription().equals(description))
.forEach(descriptionfilter -> descriptionfilter.setSelected(false))
);
}
}
}

View File

@ -0,0 +1,540 @@
/*
* 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.timeline.ui.detailview;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.eventbus.Subscribe;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Effect;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.util.Duration;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils;
import org.joda.time.DateTime;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.events.TagsAddedEvent;
import org.sleuthkit.autopsy.timeline.events.TagsDeletedEvent;
import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane;
import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider;
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show;
import static org.sleuthkit.autopsy.timeline.ui.detailview.MultiEventNodeBase.CORNER_RADII_3;
import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
/**
*
*/
public abstract class EventNodeBase<Type extends TimeLineEvent> extends StackPane implements ContextMenuProvider {
private static final Logger LOGGER = Logger.getLogger(EventNodeBase.class.getName());
private static final Image HASH_HIT = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N NON-NLS
private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N
private static final Image PIN = new Image("/org/sleuthkit/autopsy/timeline/images/marker--plus.png"); // NON-NLS //NOI18N
private static final Image UNPIN = new Image("/org/sleuthkit/autopsy/timeline/images/marker--minus.png"); // NON-NLS //NOI18N
private static final Map<EventType, Effect> dropShadowMap = new ConcurrentHashMap<>();
static void configureActionButton(ButtonBase b) {
b.setMinSize(16, 16);
b.setMaxSize(16, 16);
b.setPrefSize(16, 16);
}
static void show(Node b, boolean show) {
b.setVisible(show);
b.setManaged(show);
}
private final Type tlEvent;
private final EventNodeBase<?> parentNode;
final DetailsChartLane<?> chartLane;
final Background highlightedBackground;
final Background defaultBackground;
final Color evtColor;
final Label countLabel = new Label();
final Label descrLabel = new Label();
final ImageView hashIV = new ImageView(HASH_HIT);
final ImageView tagIV = new ImageView(TAG);
final ImageView eventTypeImageView = new ImageView();
final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading());
final HBox controlsHBox = new HBox(5);
final HBox infoHBox = new HBox(5, eventTypeImageView, hashIV, tagIV, descrLabel, countLabel, controlsHBox);
final SleuthkitCase sleuthkitCase;
final FilteredEventsModel eventsModel;
private Timeline timeline;
private Button pinButton;
private final Border SELECTION_BORDER;
EventNodeBase(Type tlEvent, EventNodeBase<?> parent, DetailsChartLane<?> chartLane) {
this.chartLane = chartLane;
this.tlEvent = tlEvent;
this.parentNode = parent;
sleuthkitCase = chartLane.getController().getAutopsyCase().getSleuthkitCase();
eventsModel = chartLane.getController().getEventsModel();
eventTypeImageView.setImage(getEventType().getFXImage());
if (tlEvent.getEventIDsWithHashHits().isEmpty()) {
show(hashIV, false);
}
if (tlEvent.getEventIDsWithTags().isEmpty()) {
show(tagIV, false);
}
if (chartLane.getController().getEventsModel().getEventTypeZoom() == EventTypeZoomLevel.SUB_TYPE) {
evtColor = getEventType().getColor();
} else {
evtColor = getEventType().getBaseType().getColor();
}
SELECTION_BORDER = new Border(new BorderStroke(evtColor.darker().desaturate(), BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2)));
defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY));
highlightedBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY));
setBackground(defaultBackground);
Tooltip.install(this, this.tooltip);
//set up mouse hover effect and tooltip
setOnMouseEntered(mouseEntered -> {
Tooltip.uninstall(chartLane, AbstractVisualizationPane.getDefaultTooltip());
showHoverControls(true);
toFront();
});
setOnMouseExited(mouseExited -> {
showHoverControls(false);
if (parentNode != null) {
parentNode.showHoverControls(true);
} else {
Tooltip.install(chartLane, AbstractVisualizationPane.getDefaultTooltip());
}
});
setOnMouseClicked(new ClickHandler());
show(controlsHBox, false);
}
public Type getEvent() {
return tlEvent;
}
@Override
public TimeLineController getController() {
return chartLane.getController();
}
public Optional<EventNodeBase<?>> getParentNode() {
return Optional.ofNullable(parentNode);
}
DetailsChartLane<?> getChartLane() {
return chartLane;
}
/**
* @param w the maximum width the description label should have
*/
public void setMaxDescriptionWidth(double w) {
descrLabel.setMaxWidth(w);
}
public abstract List<EventNodeBase<?>> getSubNodes();
/**
* apply the 'effect' to visually indicate selection
*
* @param applied true to apply the selection 'effect', false to remove it
*/
public void applySelectionEffect(boolean applied) {
setBorder(applied ? SELECTION_BORDER : null);
}
protected void layoutChildren() {
super.layoutChildren();
}
/**
* Install whatever buttons are visible on hover for this node. likes
* tooltips, this had a surprisingly large impact on speed of loading the
* chart
*/
void installActionButtons() {
if (pinButton == null) {
pinButton = new Button();
controlsHBox.getChildren().add(pinButton);
configureActionButton(pinButton);
}
}
final void showHoverControls(final boolean showControls) {
Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
eventType -> new DropShadow(-10, eventType.getColor()));
setEffect(showControls ? dropShadow : null);
installTooltip();
enableTooltip(showControls);
installActionButtons();
TimeLineController controller = getChartLane().getController();
if (controller.getPinnedEvents().contains(tlEvent)) {
pinButton.setOnAction(actionEvent -> {
new UnPinEventAction(controller, tlEvent).handle(actionEvent);
showHoverControls(true);
});
pinButton.setGraphic(new ImageView(UNPIN));
} else {
pinButton.setOnAction(actionEvent -> {
new PinEventAction(controller, tlEvent).handle(actionEvent);
showHoverControls(true);
});
pinButton.setGraphic(new ImageView(PIN));
}
show(controlsHBox, showControls);
if (parentNode != null) {
parentNode.showHoverControls(false);
}
}
/**
* defer tooltip content creation till needed, this had a surprisingly large
* impact on speed of loading the chart
*/
@NbBundle.Messages({"# {0} - counts",
"# {1} - event type",
"# {2} - description",
"# {3} - start date/time",
"# {4} - end date/time",
"EventNodeBase.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}",
"EventNodeBase.toolTip.loading2=loading tooltip",
"# {0} - hash set count string",
"EventNodeBase.toolTip.hashSetHits=\n\nHash Set Hits\n{0}",
"# {0} - tag count string",
"EventNodeBase.toolTip.tags=\n\nTags\n{0}"})
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
void installTooltip() {
if (tooltip.getText().equalsIgnoreCase(Bundle.EventBundleNodeBase_toolTip_loading())) {
final Task<String> tooltTipTask = new Task<String>() {
{
updateTitle(Bundle.EventNodeBase_toolTip_loading2());
}
@Override
protected String call() throws Exception {
HashMap<String, Long> hashSetCounts = new HashMap<>();
if (tlEvent.getEventIDsWithHashHits().isEmpty() == false) {
try {
//TODO:push this to DB
for (SingleEvent tle : eventsModel.getEventsById(tlEvent.getEventIDsWithHashHits())) {
Set<String> hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames();
for (String hashSetName : hashSetNames) {
hashSetCounts.merge(hashSetName, 1L, Long::sum);
}
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex); //NON-NLS
}
}
String hashSetCountsString = hashSetCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
Map<String, Long> tagCounts = new HashMap<>();
if (tlEvent.getEventIDsWithTags().isEmpty() == false) {
tagCounts.putAll(eventsModel.getTagCountsByTagName(tlEvent.getEventIDsWithTags()));
}
String tagCountsString = tagCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
return Bundle.EventNodeBase_tooltip_text(getEventIDs().size(), getEventType(), getDescription(),
TimeLineController.getZonedFormatter().print(getStartMillis()),
TimeLineController.getZonedFormatter().print(getEndMillis() + 1000))
+ (hashSetCountsString.isEmpty() ? "" : Bundle.EventNodeBase_toolTip_hashSetHits(hashSetCountsString))
+ (tagCountsString.isEmpty() ? "" : Bundle.EventNodeBase_toolTip_tags(tagCountsString));
}
@Override
protected void succeeded() {
super.succeeded();
try {
tooltip.setText(get());
tooltip.setGraphic(null);
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex); //NON-NLS
}
}
};
new Thread(tooltTipTask).start();
chartLane.getController().monitorTask(tooltTipTask);
}
}
void enableTooltip(boolean toolTipEnabled) {
if (toolTipEnabled) {
Tooltip.install(this, tooltip);
} else {
Tooltip.uninstall(this, tooltip);
}
}
final EventType getEventType() {
return tlEvent.getEventType();
}
long getStartMillis() {
return tlEvent.getStartMillis();
}
final long getEndMillis() {
return tlEvent.getEndMillis();
}
final double getLayoutXCompensation() {
return parentNode != null
? getChartLane().getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis()))
: 0;
}
abstract String getDescription();
void animateTo(double xLeft, double yTop) {
if (timeline != null) {
timeline.stop();
Platform.runLater(this::requestChartLayout);
}
timeline = new Timeline(new KeyFrame(Duration.millis(100),
new KeyValue(layoutXProperty(), xLeft),
new KeyValue(layoutYProperty(), yTop))
);
timeline.setOnFinished(finished -> Platform.runLater(this::requestChartLayout));
timeline.play();
}
void requestChartLayout() {
getChartLane().requestChartLayout();
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
void setDescriptionVisibility(DescriptionVisibility descrVis) {
final int size = getEvent().getSize();
switch (descrVis) {
case HIDDEN:
hideDescription();
break;
case COUNT_ONLY:
showCountOnly(size);
break;
case SHOWN:
default:
showFullDescription(size);
break;
}
}
void showCountOnly(final int size) {
descrLabel.setText("");
countLabel.setText(String.valueOf(size));
}
void hideDescription() {
countLabel.setText("");
descrLabel.setText("");
}
/**
* apply the 'effect' to visually indicate highlighted nodes
*
* @param applied true to apply the highlight 'effect', false to remove it
*/
synchronized void applyHighlightEffect(boolean applied) {
if (applied) {
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
setBackground(highlightedBackground);
} else {
descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS
setBackground(defaultBackground);
}
}
void applyHighlightEffect() {
applyHighlightEffect(true);
}
void clearHighlightEffect() {
applyHighlightEffect(false);
}
abstract Collection<Long> getEventIDs();
abstract EventHandler<MouseEvent> getDoubleClickHandler();
Iterable<? extends Action> getActions() {
if (getController().getPinnedEvents().contains(getEvent())) {
return Arrays.asList(new UnPinEventAction(getController(), getEvent()));
} else {
return Arrays.asList(new PinEventAction(getController(), getEvent()));
}
}
@Deprecated
@Override
final public void clearContextMenu() {
}
public ContextMenu getContextMenu(MouseEvent mouseEvent) {
ContextMenu chartContextMenu = chartLane.getContextMenu(mouseEvent);
ContextMenu contextMenu = ActionUtils.createContextMenu(Lists.newArrayList(getActions()));
contextMenu.getItems().add(new SeparatorMenuItem());
contextMenu.getItems().addAll(chartContextMenu.getItems());
contextMenu.setAutoHide(true);
return contextMenu;
}
void showFullDescription(final int size) {
countLabel.setText((size == 1) ? "" : " (" + size + ")"); // NON-NLS
String description = getParentNode().map(pNode ->
" ..." + StringUtils.substringAfter(getEvent().getDescription(), parentNode.getDescription()))
.orElseGet(getEvent()::getDescription);
descrLabel.setText(description);
}
@Subscribe
public void handleTimeLineTagEvent(TagsAddedEvent event) {
if (false == Sets.intersection(getEvent().getEventIDs(), event.getUpdatedEventIDs()).isEmpty()) {
Platform.runLater(() -> {
show(tagIV, true);
});
}
}
/**
* TODO: this method implementation is wrong and just a place holder
*/
@Subscribe
public void handleTimeLineTagEvent(TagsDeletedEvent event) {
Sets.SetView<Long> difference = Sets.difference(getEvent().getEventIDs(), event.getUpdatedEventIDs());
if (false == difference.isEmpty()) {
Platform.runLater(() -> {
show(tagIV, true);
});
}
}
private static class PinEventAction extends Action {
@NbBundle.Messages({"PinEventAction.text=Pin"})
PinEventAction(TimeLineController controller, TimeLineEvent event) {
super(Bundle.PinEventAction_text());
setEventHandler(actionEvent -> controller.pinEvent(event));
setGraphic(new ImageView(PIN));
}
}
private static class UnPinEventAction extends Action {
@NbBundle.Messages({"UnPinEventAction.text=Unpin"})
UnPinEventAction(TimeLineController controller, TimeLineEvent event) {
super(Bundle.UnPinEventAction_text());
setEventHandler(actionEvent -> controller.unPinEvent(event));
setGraphic(new ImageView(UNPIN));
}
}
/**
* event handler used for mouse events on {@link EventNodeBase}s
*/
private class ClickHandler implements EventHandler<MouseEvent> {
@Override
public void handle(MouseEvent t) {
if (t.getButton() == MouseButton.PRIMARY) {
if (t.getClickCount() > 1) {
getDoubleClickHandler().handle(t);
} else if (t.isShiftDown()) {
chartLane.getSelectedNodes().add(EventNodeBase.this);
} else if (t.isShortcutDown()) {
chartLane.getSelectedNodes().removeAll(EventNodeBase.this);
} else {
chartLane.getSelectedNodes().setAll(EventNodeBase.this);
}
t.consume();
} else if (t.isPopupTrigger() && t.isStillSincePress()) {
getContextMenu(t).show(EventNodeBase.this, t.getScreenX(), t.getScreenY());
t.consume();
}
}
}
}

View File

@ -1,8 +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");
@ -19,131 +18,89 @@
*/ */
package org.sleuthkit.autopsy.timeline.ui.detailview; package org.sleuthkit.autopsy.timeline.ui.detailview;
import com.google.common.collect.Iterables;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.OverrunStyle; import javafx.scene.control.OverrunStyle;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.action.Action; import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils; import org.controlsfx.control.action.ActionUtils;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChart.HideDescriptionAction;
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configureActionButton;
/** /**
* Node used in {@link EventDetailsChart} to represent an EventStripe. * Node used in {@link EventDetailsChart} to represent an EventStripe.
*/ */
final public class EventStripeNode extends EventBundleNodeBase<EventStripe, EventCluster, EventClusterNode> { final public class EventStripeNode extends MultiEventNodeBase<EventStripe, EventCluster, EventClusterNode> {
private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName()); private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName());
private Action newHideAction() {
return new HideDescriptionAction(getDescription(), getEvent().getDescriptionLoD(), chartLane.getParentChart());
}
private Button hideButton; private Button hideButton;
/**
* Pane that contains EventStripeNodes for any 'subevents' if they are
* displayed
*
* //TODO: move more of the control of subnodes/events here and out of
* EventDetail Chart
*/
// private final HBox clustersHBox = new HBox();
private final ImageView eventTypeImageView = new ImageView();
@Override EventStripeNode(DetailsChartLane<?> chartLane, EventStripe eventStripe, EventClusterNode parentNode) {
void installActionButtons() { super(chartLane, eventStripe, parentNode);
if (hideButton == null) { setMinHeight(24);
hideButton = ActionUtils.createButton(chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD()),
ActionUtils.ActionTextBehavior.HIDE);
configureLoDButton(hideButton);
infoHBox.getChildren().add(hideButton);
}
}
public EventStripeNode(EventDetailsChart chart, EventStripe eventStripe, EventClusterNode parentNode) {
super(chart, eventStripe, parentNode);
setMinHeight(48);
//setup description label //setup description label
eventTypeImageView.setImage(getEventType().getFXImage());
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
descrLabel.setGraphic(eventTypeImageView);
descrLabel.setPrefWidth(USE_COMPUTED_SIZE); descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
setAlignment(subNodePane, Pos.BOTTOM_LEFT); setAlignment(subNodePane, Pos.BOTTOM_LEFT);
if (eventStripe.getClusters().size() > 1) {
for (EventCluster cluster : eventStripe.getClusters()) { for (EventCluster cluster : eventStripe.getClusters()) {
subNodes.add(createChildNode(cluster)); subNodes.add(createChildNode(cluster.withParent(eventStripe)));
} }
getChildren().addAll(new VBox(infoHBox, subNodePane)); getChildren().addAll(new VBox(infoHBox, subNodePane));
} else {
EventNodeBase<?> childNode;
EventCluster cluster = Iterables.getOnlyElement(eventStripe.getClusters()).withParent(eventStripe);
if (cluster.getEventIDs().size() == 1) {
childNode = createChildNode(cluster);
} else {
EventClusterNode eventClusterNode = (EventClusterNode) createChildNode(cluster);
eventClusterNode.installActionButtons();
controlsHBox.getChildren().addAll(eventClusterNode.minusButton, eventClusterNode.plusButton);
eventClusterNode.infoHBox.getChildren().remove(eventClusterNode.countLabel);
childNode = eventClusterNode;
} }
@Override childNode.setDescriptionVisibility(DescriptionVisibility.HIDDEN);
EventClusterNode createChildNode(EventCluster cluster) { subNodes.add(childNode);
return new EventClusterNode(chart, cluster, this); getChildren().addAll(infoHBox, subNodePane);
} }
@Override
void showHoverControls(final boolean showControls) {
super.showHoverControls(showControls);
installActionButtons();
show(hideButton, showControls);
} }
public EventStripe getEventStripe() { public EventStripe getEventStripe() {
return getEventBundle(); return getEvent();
} }
/**
* @param w the maximum width the description label should have
*/
@Override @Override
public void setMaxDescriptionWidth(double w) { void installActionButtons() {
descrLabel.setMaxWidth(w); super.installActionButtons();
if (chartLane.quickHideFiltersEnabled() && hideButton == null) {
hideButton = ActionUtils.createButton(newHideAction(),
ActionUtils.ActionTextBehavior.HIDE);
configureActionButton(hideButton);
controlsHBox.getChildren().add(hideButton);
}
} }
/**
* apply the 'effect' to visually indicate highlighted nodes
*
* @param applied true to apply the highlight 'effect', false to remove it
*/
@Override @Override
public synchronized void applyHighlightEffect(boolean applied) { EventNodeBase<?> createChildNode(EventCluster cluster) {
if (applied) { if (cluster.getEventIDs().size() == 1) {
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS return new SingleEventNode(getChartLane(), getChartLane().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this);
setBackground(highlightedBackground);
} else { } else {
descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS return new EventClusterNode(getChartLane(), cluster, this);
setBackground(defaultBackground);
}
}
@Override
void setDescriptionVisibiltiyImpl(DescriptionVisibility descrVis) {
final int size = getEventStripe().getCount();
switch (descrVis) {
case HIDDEN:
countLabel.setText("");
descrLabel.setText("");
break;
case COUNT_ONLY:
descrLabel.setText("");
countLabel.setText(String.valueOf(size));
break;
default:
case SHOWN:
String description = getEventStripe().getDescription();
description = parentNode != null
? " ..." + StringUtils.substringAfter(description, parentNode.getDescription())
: description;
descrLabel.setText(description);
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
break;
} }
} }
@ -154,7 +111,11 @@ final public class EventStripeNode extends EventBundleNodeBase<EventStripe, Even
} }
@Override @Override
Collection<? extends Action> getActions() { Iterable<? extends Action> getActions() {
return Arrays.asList(chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD())); return Iterables.concat(
super.getActions(),
Arrays.asList(newHideAction())
);
} }
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2014-15 Basis Technology Corp. * Copyright 2014-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");
@ -38,7 +38,7 @@ class GuideLine extends Line {
private static final Tooltip CHART_DEFAULT_TOOLTIP = AbstractVisualizationPane.getDefaultTooltip(); private static final Tooltip CHART_DEFAULT_TOOLTIP = AbstractVisualizationPane.getDefaultTooltip();
private final Tooltip tooltip = new Tooltip(); private final Tooltip tooltip = new Tooltip();
private final EventDetailsChart chart; private final DetailsChart chart;
//used across invocations of mouse event handlers to maintain state //used across invocations of mouse event handlers to maintain state
private double startLayoutX; private double startLayoutX;
@ -47,7 +47,7 @@ class GuideLine extends Line {
/** /**
* @param chart the chart this GuideLine belongs to. * @param chart the chart this GuideLine belongs to.
*/ */
GuideLine(EventDetailsChart chart) { GuideLine(DetailsChart chart) {
super(0, 0, 0, 0); super(0, 0, 0, 0);
this.chart = chart; this.chart = chart;
Axis<DateTime> xAxis = chart.getXAxis(); Axis<DateTime> xAxis = chart.getXAxis();
@ -64,8 +64,8 @@ class GuideLine extends Line {
setOnMouseClicked(clickedEvent -> { setOnMouseClicked(clickedEvent -> {
if (clickedEvent.getButton() == MouseButton.SECONDARY if (clickedEvent.getButton() == MouseButton.SECONDARY
&& clickedEvent.isStillSincePress() == false) { && clickedEvent.isStillSincePress()) {
chart.clearGuideLine(); chart.clearGuideLine(this);
clickedEvent.consume(); clickedEvent.consume();
} }
}); });

View File

@ -0,0 +1,129 @@
/*
* 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.timeline.ui.detailview;
import java.util.List;
import java.util.Set;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import org.joda.time.DateTime;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
/**
*
*/
@NbBundle.Messages({"EventBundleNodeBase.toolTip.loading=loading..."})
public abstract class MultiEventNodeBase< BundleType extends MultiEvent<ParentType>, ParentType extends MultiEvent<BundleType>, ParentNodeType extends MultiEventNodeBase<
ParentType, BundleType, ?>> extends EventNodeBase<BundleType> {
private static final Logger LOGGER = Logger.getLogger(MultiEventNodeBase.class.getName());
static final CornerRadii CORNER_RADII_3 = new CornerRadii(3);
static final CornerRadii CORNER_RADII_1 = new CornerRadii(1);
final ObservableList<EventNodeBase<?>> subNodes = FXCollections.observableArrayList();
final Pane subNodePane = new Pane();
private final ReadOnlyObjectWrapper<DescriptionLoD> descLOD = new ReadOnlyObjectWrapper<>();
MultiEventNodeBase(DetailsChartLane<?> chartLane, BundleType event, ParentNodeType parentNode) {
super(event, parentNode, chartLane);
setDescriptionLOD(event.getDescriptionLoD());
setAlignment(Pos.TOP_LEFT);
setMaxWidth(USE_PREF_SIZE);
infoHBox.setMaxWidth(USE_PREF_SIZE);
subNodePane.setPrefWidth(USE_COMPUTED_SIZE);
subNodePane.setMinWidth(USE_PREF_SIZE);
subNodePane.setMaxWidth(USE_PREF_SIZE);
/*
* This triggers the layout when a mousover causes the action buttons to
* interesect with another node, forcing it down.
*/
heightProperty().addListener(heightProp -> chartLane.requestLayout());
Platform.runLater(() ->
setLayoutX(chartLane.getXAxis().getDisplayPosition(new DateTime(event.getStartMillis())) - getLayoutXCompensation())
);
//initialize info hbox
infoHBox.setPadding(new Insets(2, 3, 2, 3));
infoHBox.setAlignment(Pos.TOP_LEFT);
Bindings.bindContent(subNodePane.getChildren(), subNodes);
}
public ReadOnlyObjectProperty<DescriptionLoD> descriptionLoDProperty() {
return descLOD.getReadOnlyProperty();
}
final DescriptionLoD getDescriptionLoD() {
return descLOD.get();
}
/**
*
*/
final void setDescriptionLOD(final DescriptionLoD descriptionLoD) {
descLOD.set(descriptionLoD);
}
@SuppressWarnings("unchecked")
public List<EventNodeBase<?>> getSubNodes() {
return subNodes;
}
final String getDescription() {
return getEvent().getDescription();
}
final Set<Long> getEventIDs() {
return getEvent().getEventIDs();
}
@Override
public Orientation getContentBias() {
return Orientation.HORIZONTAL;
}
@Override
protected void layoutChildren() {
chartLane.layoutEventBundleNodes(subNodes, 0);
super.layoutChildren();
}
abstract EventNodeBase<?> createChildNode(ParentType rawChild);
abstract EventHandler<MouseEvent> getDoubleClickHandler();
}

View File

@ -0,0 +1,61 @@
/*
* 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.timeline.ui.detailview;
import javafx.collections.SetChangeListener;
import javafx.scene.chart.Axis;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
/**
*
*/
public final class PinnedEventsChartLane extends DetailsChartLane<TimeLineEvent> {
/**
*
* @param controller the value of controller
* @param dateAxis the value of dateAxis
* @param verticalAxis the value of verticalAxis
* @param selectedNodes1 the value of selectedNodes1
*/
PinnedEventsChartLane(DetailsChart parentChart, DateAxis dateAxis, final Axis<TimeLineEvent> verticalAxis) {
super(parentChart, dateAxis, verticalAxis, false);
// final Series<DateTime, TimeLineEvent> series = new Series<>();
// setData(FXCollections.observableArrayList());
// getData().add(series);
getController().getPinnedEvents().addListener((SetChangeListener.Change<? extends TimeLineEvent> change) -> {
if (change.wasAdded()) {
addEvent(change.getElementAdded());
}
if (change.wasRemoved()) {
removeEvent(change.getElementRemoved());
}
requestChartLayout();
});
getController().getPinnedEvents().stream().forEach(this::addEvent);
requestChartLayout();
}
@Override
void doAdditionalLayout() {
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.timeline.ui.detailview;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javafx.collections.ListChangeListener;
import javafx.scene.chart.Axis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.shape.Line;
import javafx.scene.shape.StrokeLineCap;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider;
/**
* Custom implementation of {@link XYChart} to graph events on a horizontal
* timeline.
*
* The horizontal {@link DateAxis} controls the tick-marks and the horizontal
* layout of the nodes representing events. The vertical {@link NumberAxis} does
* nothing (although a custom implementation could help with the vertical
* layout?)
*
* Series help organize events for the banding by event type, we could add a
* node to contain each band if we need a place for per band controls.
*
* //TODO: refactor the projected lines to a separate class. -jm
*/
public final class PrimaryDetailsChartLane extends DetailsChartLane<EventStripe> implements ContextMenuProvider {
private static final int PROJECTED_LINE_Y_OFFSET = 5;
private static final int PROJECTED_LINE_STROKE_WIDTH = 5;
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final Map<EventCluster, Line> projectionMap = new ConcurrentHashMap<>();
PrimaryDetailsChartLane(DetailsChart parentChart, DateAxis dateAxis, final Axis<EventStripe> verticalAxis) {
super(parentChart, dateAxis, verticalAxis, true);
//add listener for events that should trigger layout
getController().getQuickHideFilters().addListener(layoutInvalidationListener);
parentChart.getRootEventStripes().addListener((ListChangeListener.Change<? extends EventStripe> change) -> {
while (change.next()) {
change.getAddedSubList().stream().forEach(this::addEvent);
change.getRemoved().stream().forEach(this::removeEvent);
}
requestChartLayout();
});
parentChart.getRootEventStripes().stream().forEach(this::addEvent);
requestChartLayout();
getSelectedNodes().addListener((ListChangeListener.Change<? extends EventNodeBase<?>> change) -> {
while (change.next()) {
change.getRemoved().forEach(removedNode -> {
removedNode.getEvent().getClusters().forEach(cluster -> {
Line removedLine = projectionMap.remove(cluster);
getChartChildren().removeAll(removedLine);
});
});
change.getAddedSubList().forEach(addedNode -> {
for (EventCluster range : addedNode.getEvent().getClusters()) {
double y = dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET;
Line line =
new Line(dateAxis.localToParent(getXForEpochMillis(range.getStartMillis()), 0).getX(), y,
dateAxis.localToParent(getXForEpochMillis(range.getEndMillis()), 0).getX(), y);
line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5));
line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
line.setStrokeLineCap(StrokeLineCap.ROUND);
projectionMap.put(range, line);
getChartChildren().add(line);
}
});
}
});
}
private double getParentXForEpochMillis(Long epochMillis) {
return getXAxis().localToParent(getXForEpochMillis(epochMillis), 0).getX();
}
void doAdditionalLayout() {
for (final Map.Entry<EventCluster, Line> entry : projectionMap.entrySet()) {
final EventCluster cluster = entry.getKey();
final Line line = entry.getValue();
line.setStartX(getParentXForEpochMillis(cluster.getStartMillis()));
line.setEndX(getParentXForEpochMillis(cluster.getEndMillis()));
line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
}
}
}

View File

@ -0,0 +1,105 @@
/*
* 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.timeline.ui.detailview;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.geometry.Orientation;
import javafx.scene.control.ScrollBar;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
/**
*
*/
class ScrollingLaneWrapper extends BorderPane {
private static final double LINE_SCROLL_PERCENTAGE = .10;
private static final double PAGE_SCROLL_PERCENTAGE = .70;
private final ScrollBar vertScrollBar = new ScrollBar();
private final Region scrollBarSpacer = new Region();
private final DetailsChartLane<?> chart;
ScrollingLaneWrapper(DetailsChartLane<?> center) {
super(center);
this.chart = center;
scrollBarSpacer.minHeightProperty().bind(chart.getXAxis().heightProperty());
//configure scrollbar
vertScrollBar.setOrientation(Orientation.VERTICAL);
vertScrollBar.maxProperty().bind(chart.maxVScrollProperty().subtract(chart.heightProperty()));
vertScrollBar.visibleAmountProperty().bind(chart.heightProperty());
vertScrollBar.visibleProperty().bind(vertScrollBar.visibleAmountProperty().greaterThanOrEqualTo(0));
VBox.setVgrow(vertScrollBar, Priority.ALWAYS);
setRight(new VBox(vertScrollBar, scrollBarSpacer));
//scrollbar value change handler. This forwards changes in scroll bar to chart
this.vertScrollBar.valueProperty().addListener((Observable observable) -> {
chart.setVScroll(vertScrollBar.getValue());
});
//request focus for keyboard scrolling
setOnMouseClicked(mouseEvent -> requestFocus());
//interpret scroll events to the scrollBar
this.setOnScroll(scrollEvent ->
vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() - scrollEvent.getDeltaY())));
//interpret scroll related keys to scrollBar
this.setOnKeyPressed((KeyEvent t) -> {
switch (t.getCode()) {
case PAGE_UP:
incrementScrollValue(-PAGE_SCROLL_PERCENTAGE);
t.consume();
break;
case PAGE_DOWN:
incrementScrollValue(PAGE_SCROLL_PERCENTAGE);
t.consume();
break;
case KP_UP:
case UP:
incrementScrollValue(-LINE_SCROLL_PERCENTAGE);
t.consume();
break;
case KP_DOWN:
case DOWN:
incrementScrollValue(LINE_SCROLL_PERCENTAGE);
t.consume();
break;
}
});
}
void reset() {
Platform.runLater(() -> {
vertScrollBar.setValue(0);
});
}
private void incrementScrollValue(double factor) {
vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() + factor * chart.getHeight()));
}
private Double clampScroll(Double value) {
return Math.max(0, Math.min(vertScrollBar.getMax() + 50, value));
}
}

View File

@ -0,0 +1,103 @@
/*
* 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.timeline.ui.detailview;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.OverrunStyle;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
/**
*
*/
final class SingleEventNode extends EventNodeBase<SingleEvent> {
private static final Logger LOGGER = Logger.getLogger(SingleEventNode.class.getName());
static void show(Node b, boolean show) {
b.setVisible(show);
b.setManaged(show);
}
static final CornerRadii CORNER_RADII_1 = new CornerRadii(1);
private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(0, 0, 0, 2);
@Override
EventHandler<MouseEvent> getDoubleClickHandler() {
return mouseEvent -> {
};
}
SingleEventNode(DetailsChartLane<?> chart, SingleEvent event, MultiEventNodeBase<?, ?, ?> parent) {
super(event, parent, chart);
this.descrLabel.setText(event.getFullDescription());
eventTypeImageView.setImage(getEventType().getFXImage());
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
descrLabel.setGraphic(eventTypeImageView);
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
setMinHeight(24);
setAlignment(Pos.CENTER_LEFT);
final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS));
setBorder(clusterBorder);
setMaxWidth(USE_PREF_SIZE);
infoHBox.setMaxWidth(USE_PREF_SIZE);
getChildren().add(infoHBox);
}
@Override
public List<EventNodeBase<?>> getSubNodes() {
return Collections.emptyList();
}
@Override
protected void layoutChildren() {
super.layoutChildren(); //To change body of generated methods, choose Tools | Templates.
}
@Override
String getDescription() {
return getEvent().getFullDescription();
}
/**
* @param w the maximum width the description label should have
*/
@Override
public void setMaxDescriptionWidth(double w) {
descrLabel.setMaxWidth(w);
}
@Override
Collection<Long> getEventIDs() {
return getEvent().getEventIDs();
}
}

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");
@ -20,7 +20,7 @@ package org.sleuthkit.autopsy.timeline.ui.detailview;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
/** /**
* Use this recursive function to flatten a tree of nodes into an single stream. * Use this recursive function to flatten a tree of nodes into an single stream.
@ -28,12 +28,12 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
* EventStripes containing the stripes for the given node and all child * EventStripes containing the stripes for the given node and all child
* eventStripes, ignoring intervening EventCluster nodes. * eventStripes, ignoring intervening EventCluster nodes.
*/ */
class StripeFlattener implements Function<EventStripeNode, Stream<EventStripe>> { class StripeFlattener implements Function<EventNodeBase<?>, Stream<TimeLineEvent>> {
@Override @Override
public Stream<EventStripe> apply(EventStripeNode node) { public Stream<TimeLineEvent> apply(EventNodeBase<?> node) {
return Stream.concat( return Stream.concat(
Stream.of(node.getEventStripe()), Stream.of(node.getEvent()),
node.getSubNodes().stream().flatMap(clusterNode -> node.getSubNodes().stream().flatMap(clusterNode ->
clusterNode.getSubNodes().stream().flatMap(this))); clusterNode.getSubNodes().stream().flatMap(this)));
} }

View File

@ -24,43 +24,36 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem;
import org.apache.commons.lang3.StringUtils;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
/** /**
* *
*/ */
class EventDescriptionTreeItem extends NavTreeItem { class EventDescriptionTreeItem extends EventsTreeItem {
/** /**
* maps a description to the child item of this item with that description * maps a description to the child item of this item with that description
*/ */
private final Map<String, EventDescriptionTreeItem> childMap = new HashMap<>(); private final Map<String, EventDescriptionTreeItem> childMap = new HashMap<>();
private final EventBundle<?> bundle; private Comparator<TreeItem<TimeLineEvent>> comparator = TreeComparator.Description;
private Comparator<TreeItem<EventBundle<?>>> comparator = TreeComparator.Description;
public EventBundle<?> getEventBundle() { EventDescriptionTreeItem(EventStripe stripe, Comparator<TreeItem<TimeLineEvent>> comp) {
return bundle;
}
EventDescriptionTreeItem(EventBundle<?> g, Comparator<TreeItem<EventBundle<?>>> comp) {
bundle = g;
comparator = comp; comparator = comp;
setValue(g); setValue(stripe);
}
@Override
public long getCount() {
return getValue().getCount();
} }
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
public void insert(Deque<EventBundle<?>> path) { public void insert(Deque<EventStripe> path) {
EventBundle<?> head = path.removeFirst(); EventStripe head = path.removeFirst();
EventDescriptionTreeItem treeItem = childMap.computeIfAbsent(head.getDescription(), description -> { String substringAfter = StringUtils.substringAfter(head.getDescription(), head.getParentStripe().map(EventStripe::getDescription).orElse(""));
EventDescriptionTreeItem treeItem = childMap.computeIfAbsent(substringAfter,
description -> {
EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(head, comparator); EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(head, comparator);
newTreeItem.setExpanded(true); newTreeItem.setExpanded(true);
childMap.put(description, newTreeItem);
getChildren().add(newTreeItem); getChildren().add(newTreeItem);
resort(comparator, false); resort(comparator, false);
return newTreeItem; return newTreeItem;
@ -71,9 +64,10 @@ class EventDescriptionTreeItem extends NavTreeItem {
} }
} }
void remove(Deque<EventBundle<?>> path) { void remove(Deque<EventStripe> path) {
EventBundle<?> head = path.removeFirst(); EventStripe head = path.removeFirst();
EventDescriptionTreeItem descTreeItem = childMap.get(head.getDescription()); String substringAfter = StringUtils.substringAfter(head.getDescription(), head.getParentStripe().map(EventStripe::getDescription).orElse(""));
EventDescriptionTreeItem descTreeItem = childMap.get(substringAfter);
if (path.isEmpty() == false) { if (path.isEmpty() == false) {
descTreeItem.remove(path); descTreeItem.remove(path);
} }
@ -84,7 +78,7 @@ class EventDescriptionTreeItem extends NavTreeItem {
} }
@Override @Override
void resort(Comparator<TreeItem<EventBundle<?>>> comp, Boolean recursive) { void resort(Comparator<TreeItem<TimeLineEvent>> comp, Boolean recursive) {
this.comparator = comp; this.comparator = comp;
FXCollections.sort(getChildren(), comp); FXCollections.sort(getChildren(), comp);
if (recursive) { if (recursive) {
@ -93,14 +87,14 @@ class EventDescriptionTreeItem extends NavTreeItem {
} }
@Override @Override
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) { public EventsTreeItem findTreeItemForEvent(TimeLineEvent event) {
if (getValue().getEventType() == t.getEventType() if (getValue().getEventType() == event.getEventType()
&& getValue().getDescription().equals(t.getDescription())) { && getValue().getDescription().equals(event.getDescription())) {
return this; return this;
} else { } else {
for (EventDescriptionTreeItem child : childMap.values()) { for (EventDescriptionTreeItem child : childMap.values()) {
final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); final EventsTreeItem findTreeItemForEvent = child.findTreeItemForEvent(event);
if (findTreeItemForEvent != null) { if (findTreeItemForEvent != null) {
return findTreeItemForEvent; return findTreeItemForEvent;
} }
@ -109,4 +103,20 @@ class EventDescriptionTreeItem extends NavTreeItem {
return null; return null;
} }
@Override
String getDisplayText() {
String text = getValue().getDescription() + " (" + getValue().getSize() + ")"; // NON-NLS
TreeItem<TimeLineEvent> parent = getParent();
if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) {
text = StringUtils.substringAfter(text, parent.getValue().getDescription());
}
return text;
}
@Override
EventType getEventType() {
return getValue().getEventType();
}
} }

View File

@ -25,34 +25,34 @@ import java.util.Map;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
class EventTypeTreeItem extends NavTreeItem { class EventTypeTreeItem extends EventsTreeItem {
/** /**
* maps a description to the child item of this item with that description * maps a description to the child item of this item with that description
*/ */
private final Map<String, EventDescriptionTreeItem> childMap = new HashMap<>(); private final Map<String, EventDescriptionTreeItem> childMap = new HashMap<>();
private Comparator<TreeItem<EventBundle<?>>> comparator = TreeComparator.Description; private Comparator<TreeItem<TimeLineEvent>> comparator = TreeComparator.Description;
private final EventType eventType;
EventTypeTreeItem(EventBundle<?> g, Comparator<TreeItem<EventBundle<?>>> comp) { EventTypeTreeItem(EventStripe stripe, Comparator<TreeItem<TimeLineEvent>> comp) {
setValue(g); setValue(null);
eventType = stripe.getEventType();
comparator = comp; comparator = comp;
} }
@Override
public long getCount() {
return getValue().getCount();
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
public void insert(Deque<EventBundle<?>> path) { public void insert(Deque<EventStripe> path) {
EventBundle<?> head = path.removeFirst(); EventStripe head = path.removeFirst();
EventDescriptionTreeItem treeItem = childMap.computeIfAbsent(head.getDescription(), description -> {
EventDescriptionTreeItem treeItem = childMap.computeIfAbsent(head.getDescription(),
description -> {
EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(head, comparator); EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(head, comparator);
newTreeItem.setExpanded(true); newTreeItem.setExpanded(true);
childMap.put(head.getDescription(), newTreeItem);
getChildren().add(newTreeItem); getChildren().add(newTreeItem);
resort(comparator, false); resort(comparator, false);
return newTreeItem; return newTreeItem;
@ -63,8 +63,8 @@ class EventTypeTreeItem extends NavTreeItem {
} }
} }
void remove(Deque<EventBundle<?>> path) { void remove(Deque<EventStripe> path) {
EventBundle<?> head = path.removeFirst(); EventStripe head = path.removeFirst();
EventDescriptionTreeItem descTreeItem = childMap.get(head.getDescription()); EventDescriptionTreeItem descTreeItem = childMap.get(head.getDescription());
if (descTreeItem != null) { if (descTreeItem != null) {
if (path.isEmpty() == false) { if (path.isEmpty() == false) {
@ -78,11 +78,11 @@ class EventTypeTreeItem extends NavTreeItem {
} }
@Override @Override
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) { public EventsTreeItem findTreeItemForEvent(TimeLineEvent t) {
if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) { if (t.getEventType().getBaseType() == eventType.getBaseType()) {
for (EventDescriptionTreeItem child : childMap.values()) { for (EventDescriptionTreeItem child : childMap.values()) {
final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); final EventsTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t);
if (findTreeItemForEvent != null) { if (findTreeItemForEvent != null) {
return findTreeItemForEvent; return findTreeItemForEvent;
} }
@ -92,11 +92,22 @@ class EventTypeTreeItem extends NavTreeItem {
} }
@Override @Override
void resort(Comparator<TreeItem<EventBundle<?>>> comp, Boolean recursive) { void resort(Comparator<TreeItem<TimeLineEvent>> comp, Boolean recursive) {
this.comparator = comp; this.comparator = comp;
FXCollections.sort(getChildren(), comp); FXCollections.sort(getChildren(), comp);
if (recursive) { if (recursive) {
childMap.values().forEach(ti -> ti.resort(comp, true)); childMap.values().forEach(ti -> ti.resort(comp, true));
} }
} }
@Override
String getDisplayText() {
return eventType.getDisplayName();
}
@Override
EventType getEventType() {
return eventType;
}
} }

View File

@ -25,7 +25,9 @@ import java.util.Objects;
import javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
@ -43,13 +45,14 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils; import org.controlsfx.control.action.ActionUtils;
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.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.FXMLConstructor;
import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
@ -66,42 +69,37 @@ final public class EventsTree extends BorderPane {
private DetailViewPane detailViewPane; private DetailViewPane detailViewPane;
@FXML @FXML
private TreeView<EventBundle<?>> eventsTree; private TreeView<TimeLineEvent> eventsTree;
@FXML @FXML
private Label eventsTreeLabel; private Label eventsTreeLabel;
@FXML @FXML
private ComboBox<TreeComparator> sortByBox; private ComboBox<TreeComparator> sortByBox;
private final ObservableList<TimeLineEvent> selectedEvents = FXCollections.observableArrayList();
public EventsTree(TimeLineController controller) { public EventsTree(TimeLineController controller) {
this.controller = controller; this.controller = controller;
FXMLConstructor.construct(this, "EventsTree.fxml"); // NON-NLS FXMLConstructor.construct(this, "EventsTree.fxml"); // NON-NLS
} }
public void setDetailViewPane(DetailViewPane detailViewPane) { public void setDetailViewPane(DetailViewPane detailViewPane) {
this.detailViewPane = detailViewPane; this.detailViewPane = detailViewPane;
detailViewPane.setSelectionModel(eventsTree.getSelectionModel());
detailViewPane.getEventStripes().addListener((ListChangeListener.Change<? extends EventBundle<?>> c) -> { detailViewPane.getAllEventStripes().addListener((ListChangeListener.Change<? extends EventStripe> c) -> {
//on jfx thread //on jfx thread
while (c.next()) { while (c.next()) {
for (EventBundle<?> bundle : c.getAddedSubList()) { c.getRemoved().forEach(getRoot()::remove);
getRoot().insert(bundle); c.getAddedSubList().forEach(getRoot()::insert);
}
for (EventBundle<?> bundle : c.getRemoved()) {
getRoot().remove(bundle);
}
} }
}); });
setRoot(); setRoot();
detailViewPane.getSelectedNodes().addListener((Observable observable) -> { detailViewPane.getSelectedEvents().addListener((Observable observable) -> {
eventsTree.getSelectionModel().clearSelection(); eventsTree.getSelectionModel().clearSelection();
detailViewPane.getSelectedNodes().forEach(eventBundleNode -> { detailViewPane.getSelectedEvents().forEach(event -> {
eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventBundleNode.getEventBundle())); eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(event));
}); });
}); });
@ -114,11 +112,8 @@ final public class EventsTree extends BorderPane {
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void setRoot() { private void setRoot() {
RootItem root = new RootItem(TreeComparator.Type.reversed().thenComparing(sortByBox.getSelectionModel().getSelectedItem())); RootItem root = new RootItem(TreeComparator.Type.reversed().thenComparing(sortByBox.getSelectionModel().getSelectedItem()));
for (EventBundle<?> bundle : detailViewPane.getEventStripes()) { detailViewPane.getAllEventStripes().forEach(root::insert);
root.insert(bundle);
}
eventsTree.setRoot(root); eventsTree.setRoot(root);
} }
@FXML @FXML
@ -130,28 +125,39 @@ final public class EventsTree extends BorderPane {
sortByBox.getSelectionModel().select(TreeComparator.Description); sortByBox.getSelectionModel().select(TreeComparator.Description);
sortByBox.setCellFactory(listView -> new TreeComparatorCell()); sortByBox.setCellFactory(listView -> new TreeComparatorCell());
sortByBox.setButtonCell(new TreeComparatorCell()); sortByBox.setButtonCell(new TreeComparatorCell());
sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> { sortByBox.getSelectionModel().selectedItemProperty().addListener(selectedItemProperty -> {
getRoot().resort(TreeComparator.Type.reversed().thenComparing(sortByBox.getSelectionModel().getSelectedItem()), true); getRoot().resort(TreeComparator.Type.reversed().thenComparing(sortByBox.getSelectionModel().getSelectedItem()), true);
}); });
eventsTree.setShowRoot(false);
eventsTree.setShowRoot(false);
eventsTree.setCellFactory(treeView -> new EventBundleTreeCell()); eventsTree.setCellFactory(treeView -> new EventBundleTreeCell());
eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
eventsTree.getSelectionModel().getSelectedItems().addListener((ListChangeListener.Change<? extends TreeItem<TimeLineEvent>> change) -> {
while (change.next()) {
change.getRemoved().stream().map(TreeItem<TimeLineEvent>::getValue).forEach(selectedEvents::remove);
change.getAddedSubList().stream().map(TreeItem<TimeLineEvent>::getValue).filter(Objects::nonNull).forEach(selectedEvents::add);
}
});
eventsTreeLabel.setText(Bundle.EventsTree_Label_text()); eventsTreeLabel.setText(Bundle.EventsTree_Label_text());
} }
public ObservableList<TimeLineEvent> getSelectedEvents() {
return selectedEvents;
}
/** /**
* A tree cell to display {@link EventBundle}s. Shows the description, and * A tree cell to display {@link EventStripe}s. Shows the description, and
* count, as well a a "legend icon" for the event type. * count, as well a a "legend icon" for the event type.
*/ */
private class EventBundleTreeCell extends TreeCell<EventBundle<?>> { private class EventBundleTreeCell extends TreeCell<TimeLineEvent> {
private static final double HIDDEN_MULTIPLIER = .6; private static final double HIDDEN_MULTIPLIER = .6;
private final Rectangle rect = new Rectangle(24, 24); private final Rectangle rect = new Rectangle(24, 24);
private final ImageView imageView = new ImageView(); private final ImageView imageView = new ImageView();
private InvalidationListener filterStateChangeListener; private InvalidationListener filterStateChangeListener;
SimpleBooleanProperty hidden = new SimpleBooleanProperty(false); private final SimpleBooleanProperty hidden = new SimpleBooleanProperty(false);
EventBundleTreeCell() { EventBundleTreeCell() {
rect.setArcHeight(5); rect.setArcHeight(5);
@ -160,51 +166,42 @@ final public class EventsTree extends BorderPane {
} }
@Override @Override
protected void updateItem(EventBundle<?> item, boolean empty) { protected void updateItem(TimeLineEvent item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (item == null || empty) { if (empty) {
setText(null); setText(null);
setTooltip(null); setTooltip(null);
setGraphic(null); setGraphic(null);
setContextMenu(null);
deRegisterListeners(controller.getQuickHideFilters()); deRegisterListeners(controller.getQuickHideFilters());
} else { } else {
filterStateChangeListener = (filterState) -> updateHiddenState(item); EventsTreeItem treeItem = (EventsTreeItem) getTreeItem();
String text = treeItem.getDisplayText();
setText(text);
setTooltip(new Tooltip(text));
imageView.setImage(treeItem.getEventType().getFXImage());
setGraphic(new StackPane(rect, imageView));
updateHiddenState(treeItem);
deRegisterListeners(controller.getQuickHideFilters());
if (item != null) {
filterStateChangeListener = (filterState) -> updateHiddenState(treeItem);
controller.getQuickHideFilters().addListener((ListChangeListener.Change<? extends DescriptionFilter> listChange) -> { controller.getQuickHideFilters().addListener((ListChangeListener.Change<? extends DescriptionFilter> listChange) -> {
while (listChange.next()) { while (listChange.next()) {
deRegisterListeners(listChange.getRemoved()); deRegisterListeners(listChange.getRemoved());
registerListeners(listChange.getAddedSubList(), item); registerListeners(listChange.getAddedSubList(), item);
} }
updateHiddenState(item); updateHiddenState(treeItem);
}); });
registerListeners(controller.getQuickHideFilters(), item); registerListeners(controller.getQuickHideFilters(), item);
String text;
if (getTreeItem() instanceof EventTypeTreeItem) {
text = item.getEventType().getDisplayName();
setDisable(true);
} else {
setDisable(false);
text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS
TreeItem<EventBundle<?>> parent = getTreeItem().getParent();
if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) {
text = StringUtils.substringAfter(text, parent.getValue().getDescription());
}
}
setText(text);
setTooltip(new Tooltip(text));
imageView.setImage(item.getEventType().getFXImage());
setGraphic(new StackPane(rect, imageView));
updateHiddenState(item);
if (getTreeItem() instanceof EventDescriptionTreeItem) {
setOnMouseClicked((MouseEvent event) -> { setOnMouseClicked((MouseEvent event) -> {
if (event.getButton() == MouseButton.SECONDARY) { if (event.getButton() == MouseButton.SECONDARY) {
if (hidden.get()) { Action action = hidden.get()
ActionUtils.createContextMenu(ImmutableList.of(detailViewPane.newUnhideDescriptionAction(item.getDescription(), item.getDescriptionLoD()))) ? detailViewPane.newUnhideDescriptionAction(item.getDescription(), item.getDescriptionLoD())
.show(EventBundleTreeCell.this, event.getScreenX(), event.getScreenY()); : detailViewPane.newHideDescriptionAction(item.getDescription(), item.getDescriptionLoD());
} else {
ActionUtils.createContextMenu(ImmutableList.of(detailViewPane.newHideDescriptionAction(item.getDescription(), item.getDescriptionLoD()))) ActionUtils.createContextMenu(ImmutableList.of(action))
.show(EventBundleTreeCell.this, event.getScreenX(), event.getScreenY()); .show(this, event.getScreenX(), event.getScreenY());
}
} }
}); });
} else { } else {
@ -213,7 +210,7 @@ final public class EventsTree extends BorderPane {
} }
} }
private void registerListeners(Collection<? extends DescriptionFilter> filters, EventBundle<?> item) { private void registerListeners(Collection<? extends DescriptionFilter> filters, TimeLineEvent item) {
for (DescriptionFilter filter : filters) { for (DescriptionFilter filter : filters) {
if (filter.getDescription().equals(item.getDescription())) { if (filter.getDescription().equals(item.getDescription())) {
filter.activeProperty().addListener(filterStateChangeListener); filter.activeProperty().addListener(filterStateChangeListener);
@ -229,25 +226,22 @@ final public class EventsTree extends BorderPane {
} }
} }
private void updateHiddenState(EventBundle<?> item) { private void updateHiddenState(EventsTreeItem treeItem) {
TreeItem<EventBundle<?>> treeItem = getTreeItem(); TimeLineEvent event = treeItem.getValue();
hidden.set(event != null && controller.getQuickHideFilters().stream().
hidden.set(controller.getQuickHideFilters().stream(). filter(DescriptionFilter::isActive)
filter(AbstractFilter::isActive) .anyMatch(filter -> StringUtils.equalsIgnoreCase(filter.getDescription(), event.getDescription())));
.anyMatch(filter -> filter.getDescription().equals(item.getDescription())));
if (hidden.get()) { if (hidden.get()) {
if (treeItem != null) {
treeItem.setExpanded(false); treeItem.setExpanded(false);
}
setTextFill(Color.gray(0, HIDDEN_MULTIPLIER)); setTextFill(Color.gray(0, HIDDEN_MULTIPLIER));
imageView.setOpacity(HIDDEN_MULTIPLIER); imageView.setOpacity(HIDDEN_MULTIPLIER);
rect.setStroke(item.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, 1, HIDDEN_MULTIPLIER)); rect.setStroke(treeItem.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, 1, HIDDEN_MULTIPLIER));
rect.setFill(item.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, HIDDEN_MULTIPLIER, 0.1)); rect.setFill(treeItem.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, HIDDEN_MULTIPLIER, 0.1));
} else { } else {
setTextFill(Color.BLACK); setTextFill(Color.BLACK);
imageView.setOpacity(1); imageView.setOpacity(1);
rect.setStroke(item.getEventType().getColor()); rect.setStroke(treeItem.getEventType().getColor());
rect.setFill(item.getEventType().getColor().deriveColor(0, 1, 1, 0.1)); rect.setFill(treeItem.getEventType().getColor().deriveColor(0, 1, 1, 0.1));
} }
} }
} }

View File

@ -20,7 +20,8 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
import java.util.Comparator; import java.util.Comparator;
import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
/** /**
* A node in the nav tree. Manages inserts and resorts. Has parents and * A node in the nav tree. Manages inserts and resorts. Has parents and
@ -28,12 +29,14 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
* {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type, * {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type,
* description , count, etc. * description , count, etc.
*/ */
abstract class NavTreeItem extends TreeItem<EventBundle<?>> { abstract class EventsTreeItem extends TreeItem<TimeLineEvent> {
abstract long getCount(); abstract void resort(Comparator<TreeItem<TimeLineEvent>> comp, Boolean recursive);
abstract void resort(Comparator<TreeItem<EventBundle<?>>> comp, Boolean recursive); abstract EventsTreeItem findTreeItemForEvent(TimeLineEvent event);
abstract NavTreeItem findTreeItemForEvent(EventBundle<?> t); abstract String getDisplayText();
abstract EventType getEventType();
} }

View File

@ -26,13 +26,14 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
/** /**
* *
*/ */
class RootItem extends NavTreeItem { class RootItem extends EventsTreeItem {
/** /**
* maps a description to the child item of this item with that description * maps a description to the child item of this item with that description
@ -42,75 +43,79 @@ class RootItem extends NavTreeItem {
/** /**
* the comparator if any used to sort the children of this item * the comparator if any used to sort the children of this item
*/ */
private Comparator<TreeItem<EventBundle<?>>> comparator = TreeComparator.Type.reversed(); private Comparator<TreeItem<TimeLineEvent>> comparator = TreeComparator.Type.reversed();
RootItem(Comparator<TreeItem<EventBundle<?>>> comp) { RootItem(Comparator<TreeItem<TimeLineEvent>> comp) {
comp = comp; this.comparator = comp;
}
@Override
public long getCount() {
return getValue().getCount();
} }
/** /**
* Recursive method to add a grouping at a given path. * Recursive method to add a grouping at a given path.
* *
* @param bundle bundle to add * @param stripe stripe to add
*/ */
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
public void insert(EventBundle<?> bundle) { public void insert(EventStripe stripe) {
EventTypeTreeItem treeItem = childMap.computeIfAbsent(bundle.getEventType().getBaseType(), EventTypeTreeItem treeItem = childMap.computeIfAbsent(stripe.getEventType().getBaseType(),
baseType -> { baseType -> {
EventTypeTreeItem newTreeItem = new EventTypeTreeItem(bundle, comparator); EventTypeTreeItem newTreeItem = new EventTypeTreeItem(stripe, comparator);
newTreeItem.setExpanded(true); newTreeItem.setExpanded(true);
getChildren().add(newTreeItem); getChildren().add(newTreeItem);
return newTreeItem; return newTreeItem;
}); });
treeItem.insert(getTreePath(bundle)); treeItem.insert(getTreePath(stripe));
} }
void remove(EventBundle<?> bundle) { void remove(EventStripe stripe) {
EventTypeTreeItem typeTreeItem = childMap.get(bundle.getEventType().getBaseType()); EventTypeTreeItem typeTreeItem = childMap.get(stripe.getEventType().getBaseType());
if (typeTreeItem != null) { if (typeTreeItem != null) {
typeTreeItem.remove(getTreePath(bundle)); typeTreeItem.remove(getTreePath(stripe));
if (typeTreeItem.getChildren().isEmpty()) { if (typeTreeItem.getChildren().isEmpty()) {
childMap.remove(bundle.getEventType().getBaseType()); childMap.remove(stripe.getEventType().getBaseType());
getChildren().remove(typeTreeItem); getChildren().remove(typeTreeItem);
} }
} }
} }
static Deque< EventBundle<?>> getTreePath(EventBundle<?> g) { static Deque< EventStripe> getTreePath(EventStripe event) {
Deque<EventBundle<?>> path = new ArrayDeque<>(); Deque<EventStripe> path = new ArrayDeque<>();
Optional<? extends EventBundle<?>> p = Optional.of(g); path.addFirst(event);
Optional<EventStripe> parentOptional = event.getParentStripe();
while (p.isPresent()) { while (parentOptional.isPresent()) {
EventBundle<?> parent = p.get(); EventStripe parent = parentOptional.get();
path.addFirst(parent); path.addFirst(parent);
p = parent.getParentBundle(); parentOptional = parent.getParentStripe();
} }
return path; return path;
} }
@Override @Override
void resort(Comparator<TreeItem<EventBundle<?>>> comp, Boolean recursive) { void resort(Comparator<TreeItem<TimeLineEvent>> comp, Boolean recursive) {
comparator = comp; comparator = comp;
childMap.values().forEach(ti -> ti.resort(comp, true)); childMap.values().forEach(ti -> ti.resort(comp, true));
} }
@Override @Override
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) { public EventsTreeItem findTreeItemForEvent(TimeLineEvent t) {
for (EventTypeTreeItem child : childMap.values()) { for (EventTypeTreeItem child : childMap.values()) {
final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); final EventsTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t);
if (findTreeItemForEvent != null) { if (findTreeItemForEvent != null) {
return findTreeItemForEvent; return findTreeItemForEvent;
} }
} }
return null; return null;
} }
@Override
String getDisplayText() {
return "";
}
@Override
EventType getEventType() {
return null;
}
} }

View File

@ -21,29 +21,29 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
import java.util.Comparator; import java.util.Comparator;
import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
@NbBundle.Messages({"TreeComparator.Description.displayName=Description", @NbBundle.Messages({"TreeComparator.Description.displayName=Description",
"TreeComparator.Count.displayName=Count", "TreeComparator.Count.displayName=Count",
"TreeComparator.Type.displayName=Type"}) "TreeComparator.Type.displayName=Type"})
enum TreeComparator implements Comparator<TreeItem<EventBundle<?>>> { enum TreeComparator implements Comparator<TreeItem<TimeLineEvent>> {
Description(Bundle.TreeComparator_Description_displayName()) { Description(Bundle.TreeComparator_Description_displayName()) {
@Override @Override
public int compare(TreeItem<EventBundle<?>> o1, TreeItem<EventBundle<?>> o2) { public int compare(TreeItem<TimeLineEvent> o1, TreeItem<TimeLineEvent> o2) {
return o1.getValue().getDescription().compareTo(o2.getValue().getDescription()); return o1.getValue().getDescription().compareTo(o2.getValue().getDescription());
} }
}, },
Count(Bundle.TreeComparator_Count_displayName()) { Count(Bundle.TreeComparator_Count_displayName()) {
@Override @Override
public int compare(TreeItem<EventBundle<?>> o1, TreeItem<EventBundle<?>> o2) { public int compare(TreeItem<TimeLineEvent> o1, TreeItem<TimeLineEvent> o2) {
return Long.compare(o2.getValue().getCount(), o1.getValue().getCount()); return Long.compare(o2.getValue().getSize(), o1.getValue().getSize());
} }
}, },
Type(Bundle.TreeComparator_Type_displayName()) { Type(Bundle.TreeComparator_Type_displayName()) {
@Override @Override
public int compare(TreeItem<EventBundle<?>> o1, TreeItem<EventBundle<?>> o2) { public int compare(TreeItem<TimeLineEvent> o1, TreeItem<TimeLineEvent> o2) {
return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType()); return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType());
} }
}; };

View File

@ -38,13 +38,14 @@ class FilterCheckBoxCellFactory<X extends AbstractFilter> extends AbstractFXCell
checkBox.selectedProperty().unbindBidirectional(selectedProperty); checkBox.selectedProperty().unbindBidirectional(selectedProperty);
} }
if (disabledProperty != null) { if (disabledProperty != null) {
checkBox.disableProperty().unbind();//disabledProperty); checkBox.disableProperty().unbind();
} }
if (item == null) { if (item == null) {
cell.setGraphic(null); cell.setGraphic(null);
} else { } else {
checkBox.setText(item.getDisplayName()); checkBox.setText(item.getDisplayName());
// cell.setText(item.getDisplayName());
selectedProperty = item.selectedProperty(); selectedProperty = item.selectedProperty();
checkBox.selectedProperty().bindBidirectional(selectedProperty); checkBox.selectedProperty().bindBidirectional(selectedProperty);
disabledProperty = item.disabledProperty(); disabledProperty = item.disabledProperty();

View File

@ -8,22 +8,30 @@
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> <fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<top><ToolBar prefWidth="200.0" BorderPane.alignment="CENTER"> <top>
<ToolBar prefWidth="200.0" BorderPane.alignment="CENTER">
<items> <items>
<Button fx:id="applyButton" mnemonicParsing="false"> <Button fx:id="applyButton" mnemonicParsing="false">
<HBox.margin> <HBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</HBox.margin> </HBox.margin>
<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>
<Image url="@../../images/tick.png" /> <Image url="@../../images/tick.png" />
</image></ImageView> </image>
</graphic></Button><Button fx:id="defaultButton" mnemonicParsing="false"> </ImageView>
<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> </graphic>
</Button>
<Button fx:id="defaultButton" mnemonicParsing="false">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image> <image>
<Image url="@../../images/arrow-circle-double-135.png" /> <Image url="@../../images/arrow-circle-double-135.png" />
</image></ImageView> </image>
</graphic></Button> </ImageView>
</graphic>
</Button>
</items> </items>
</ToolBar> </ToolBar>
</top> </top>
@ -40,10 +48,16 @@
</columnResizePolicy> </columnResizePolicy>
</TreeTableView> </TreeTableView>
<TitledPane fx:id="hiddenDescriptionsPane" collapsible="false" disable="true" expanded="false" minHeight="25.0" text="Hidden Descriptions"> <TitledPane fx:id="hiddenDescriptionsPane" collapsible="false" disable="true" expanded="false" minHeight="25.0" text="Hidden Descriptions">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<Image url="@../../images/eye_close.png" />
</ImageView>
</graphic>
<content> <content>
<ListView fx:id="hiddenDescriptionsListView" /> <ListView fx:id="hiddenDescriptionsListView" />
</content> </content>
</TitledPane> </TitledPane>
</items> </items>
</SplitPane> </SplitPane>
</center></fx:root> </center>
</fx:root>

View File

@ -18,23 +18,20 @@
*/ */
package org.sleuthkit.autopsy.timeline.ui.filtering; package org.sleuthkit.autopsy.timeline.ui.filtering;
import java.util.Arrays;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableMap; import javafx.collections.ObservableMap;
import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu; import javafx.scene.control.Cell;
import javafx.scene.control.ListCell; import javafx.scene.control.ListCell;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitPane; import javafx.scene.control.SplitPane;
import javafx.scene.control.TitledPane; import javafx.scene.control.TitledPane;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView; import javafx.scene.control.TreeTableView;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
@ -86,169 +83,71 @@ final public class FilterSetPanel extends BorderPane {
@FXML @FXML
private SplitPane splitPane; private SplitPane splitPane;
private FilteredEventsModel filteredEvents; private final FilteredEventsModel filteredEvents;
private final TimeLineController controller;
private TimeLineController controller;
private final ObservableMap<String, Boolean> expansionMap = FXCollections.observableHashMap(); private final ObservableMap<String, Boolean> expansionMap = FXCollections.observableHashMap();
private double position; private double dividerPosition;
@FXML
@NbBundle.Messages({ @NbBundle.Messages({
"Timeline.ui.filtering.menuItem.all=all",
"FilterSetPanel.defaultButton.text=Default", "FilterSetPanel.defaultButton.text=Default",
"Timeline.ui.filtering.menuItem.none=none",
"Timeline.ui.filtering.menuItem.only=only",
"Timeline.ui.filtering.menuItem.others=others",
"Timeline.ui.filtering.menuItem.select=select",
"FilterSetPanel.hiddenDescriptionsListView.unhideAndRm=Unhide and remove from list",
"FilterSetPanel.hiddenDescriptionsListView.remove=Remove from list",
"FilsetSetPanel.hiddenDescriptionsPane.displayName=Hidden Descriptions"}) "FilsetSetPanel.hiddenDescriptionsPane.displayName=Hidden Descriptions"})
@FXML
void initialize() { void initialize() {
assert applyButton != null : "fx:id=\"applyButton\" was not injected: check your FXML file 'FilterSetPanel.fxml'."; // NON-NLS assert applyButton != null : "fx:id=\"applyButton\" was not injected: check your FXML file 'FilterSetPanel.fxml'."; // NON-NLS
ActionUtils.configureButton(new ApplyFiltersAction(), applyButton); ActionUtils.configureButton(new ApplyFiltersAction(), applyButton);
defaultButton.setText(Bundle.FilterSetPanel_defaultButton_text()); ActionUtils.configureButton(new ResetFilters(Bundle.FilterSetPanel_defaultButton_text(), controller), defaultButton);
hiddenDescriptionsPane.setText(Bundle.FilsetSetPanel_hiddenDescriptionsPane_displayName()); hiddenDescriptionsPane.setText(Bundle.FilsetSetPanel_hiddenDescriptionsPane_displayName());
//remove column headers via css. //remove column headers via css.
filterTreeTable.getStylesheets().addAll(FilterSetPanel.class.getResource("FilterTable.css").toExternalForm()); // NON-NLS filterTreeTable.getStylesheets().addAll(FilterSetPanel.class.getResource("FilterTable.css").toExternalForm()); // NON-NLS
//use row factory as hook to attach context menus to. //use row factory as hook to attach context menus to.
filterTreeTable.setRowFactory((TreeTableView<Filter> param) -> { filterTreeTable.setRowFactory(ttv -> new FilterTreeTableRow());
final TreeTableRow<Filter> row = new TreeTableRow<>();
MenuItem all = new MenuItem(Bundle.Timeline_ui_filtering_menuItem_all());
all.setOnAction(e -> {
row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> {
t.getValue().setSelected(Boolean.TRUE);
});
});
MenuItem none = new MenuItem(Bundle.Timeline_ui_filtering_menuItem_none());
none.setOnAction(e -> {
row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> {
t.getValue().setSelected(Boolean.FALSE);
});
});
MenuItem only = new MenuItem(Bundle.Timeline_ui_filtering_menuItem_only());
only.setOnAction(e -> {
row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> {
if (t == row.getTreeItem()) {
t.getValue().setSelected(Boolean.TRUE);
} else {
t.getValue().setSelected(Boolean.FALSE);
}
});
});
MenuItem others = new MenuItem(Bundle.Timeline_ui_filtering_menuItem_others());
others.setOnAction(e -> {
row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> {
if (t == row.getTreeItem()) {
t.getValue().setSelected(Boolean.FALSE);
} else {
t.getValue().setSelected(Boolean.TRUE);
}
});
});
final ContextMenu rowMenu = new ContextMenu();
Menu select = new Menu(Bundle.Timeline_ui_filtering_menuItem_select());
select.setOnAction(e -> {
row.getItem().setSelected(!row.getItem().isSelected());
});
select.getItems().addAll(all, none, only, others);
rowMenu.getItems().addAll(select);
row.setContextMenu(rowMenu);
return row;
});
//configure tree column to show name of filter and checkbox //configure tree column to show name of filter and checkbox
treeColumn.setCellValueFactory(param -> param.getValue().valueProperty()); treeColumn.setCellValueFactory(cellDataFeatures -> cellDataFeatures.getValue().valueProperty());
treeColumn.setCellFactory(col -> new FilterCheckBoxCellFactory<>().forTreeTable(col)); treeColumn.setCellFactory(col -> new FilterCheckBoxCellFactory<>().forTreeTable(col));
//configure legend column to show legend (or othe supplamantal ui, eg, text field for text filter) //configure legend column to show legend (or othe supplamantal ui, eg, text field for text filter)
legendColumn.setCellValueFactory(param -> param.getValue().valueProperty()); legendColumn.setCellValueFactory(cellDataFeatures -> cellDataFeatures.getValue().valueProperty());
legendColumn.setCellFactory(col -> new LegendCell(this.controller)); legendColumn.setCellFactory(col -> new LegendCell(this.controller));
expansionMap.put(new TypeFilter(RootEventType.getInstance()).getDisplayName(), true); expansionMap.put(new TypeFilter(RootEventType.getInstance()).getDisplayName(), true);
Action defaultFiltersAction = new ResetFilters(controller); this.filteredEvents.eventTypeZoomProperty().addListener((Observable observable) -> applyFilters());
defaultButton.setOnAction(defaultFiltersAction); this.filteredEvents.descriptionLODProperty().addListener((Observable observable1) -> applyFilters());
defaultButton.disableProperty().bind(defaultFiltersAction.disabledProperty()); this.filteredEvents.timeRangeProperty().addListener((Observable observable2) -> applyFilters());
this.filteredEvents.eventTypeZoomProperty().addListener((Observable observable) -> { this.filteredEvents.filterProperty().addListener((Observable o) -> refresh());
applyFilters();
});
this.filteredEvents.descriptionLODProperty().addListener((Observable observable1) -> {
applyFilters();
});
this.filteredEvents.timeRangeProperty().addListener((Observable observable2) -> {
applyFilters();
});
this.filteredEvents.filterProperty().addListener((Observable o) -> {
refresh();
});
refresh(); refresh();
hiddenDescriptionsListView.setItems(controller.getQuickHideFilters()); hiddenDescriptionsListView.setItems(controller.getQuickHideFilters());
hiddenDescriptionsListView.setCellFactory((ListView<DescriptionFilter> param) -> { hiddenDescriptionsListView.setCellFactory(listView -> getNewDiscriptionFilterListCell());
final ListCell<DescriptionFilter> forList = new FilterCheckBoxCellFactory<DescriptionFilter>().forList();
forList.itemProperty().addListener((Observable observable) -> {
if (forList.getItem() == null) {
forList.setContextMenu(null);
} else {
forList.setContextMenu(new ContextMenu(new MenuItem() {
{
forList.getItem().selectedProperty().addListener((observable, wasSelected, isSelected) -> {
configureText(isSelected);
});
configureText(forList.getItem().selectedProperty().get());
setOnAction((ActionEvent event) -> {
controller.getQuickHideFilters().remove(forList.getItem());
});
}
private void configureText(Boolean newValue) {
if (newValue) {
setText(Bundle.FilterSetPanel_hiddenDescriptionsListView_unhideAndRm());
} else {
setText(Bundle.FilterSetPanel_hiddenDescriptionsListView_remove());
}
}
}));
}
});
return forList;
});
controller.viewModeProperty().addListener(observable -> { controller.viewModeProperty().addListener(observable -> {
applyFilters(); applyFilters();
if (controller.viewModeProperty().get() == VisualizationMode.COUNTS) { if (controller.viewModeProperty().get() == VisualizationMode.COUNTS) {
position = splitPane.getDividerPositions()[0]; dividerPosition = splitPane.getDividerPositions()[0];
splitPane.setDividerPositions(1); splitPane.setDividerPositions(1);
hiddenDescriptionsPane.setExpanded(false); hiddenDescriptionsPane.setExpanded(false);
hiddenDescriptionsPane.setCollapsible(false); hiddenDescriptionsPane.setCollapsible(false);
hiddenDescriptionsPane.setDisable(true); hiddenDescriptionsPane.setDisable(true);
} else { } else {
splitPane.setDividerPositions(position); splitPane.setDividerPositions(dividerPosition);
hiddenDescriptionsPane.setDisable(false); hiddenDescriptionsPane.setDisable(false);
hiddenDescriptionsPane.setCollapsible(true); hiddenDescriptionsPane.setCollapsible(true);
hiddenDescriptionsPane.setExpanded(true); hiddenDescriptionsPane.setExpanded(true);
hiddenDescriptionsPane.setCollapsible(false); hiddenDescriptionsPane.setCollapsible(false);
} }
}); });
} }
public FilterSetPanel(TimeLineController controller) { public FilterSetPanel(TimeLineController controller) {
this.controller = controller; this.controller = controller;
this.filteredEvents = controller.getEventsModel(); this.filteredEvents = controller.getEventsModel();
FXMLConstructor.construct(this, "FilterSetPanel.fxml"); // NON-NLS FXMLConstructor.construct(this, "FilterSetPanel.fxml"); // NON-NLS
} }
private void refresh() { private void refresh() {
@ -257,6 +156,26 @@ final public class FilterSetPanel extends BorderPane {
}); });
} }
private void applyFilters() {
Platform.runLater(() -> {
controller.pushFilters((RootFilter) filterTreeTable.getRoot().getValue());
});
}
private ListCell<DescriptionFilter> getNewDiscriptionFilterListCell() {
final ListCell<DescriptionFilter> cell = new FilterCheckBoxCellFactory<DescriptionFilter>().forList();
cell.itemProperty().addListener(itemProperty -> {
if (cell.getItem() == null) {
cell.setContextMenu(null);
} else {
cell.setContextMenu(ActionUtils.createContextMenu(Arrays.asList(
new RemoveDescriptionFilterAction(controller, cell))
));
}
});
return cell;
}
@NbBundle.Messages({"FilterSetPanel.applyButton.text=Apply", @NbBundle.Messages({"FilterSetPanel.applyButton.text=Apply",
"FilterSetPanel.applyButton.longText=(Re)Apply filters"}) "FilterSetPanel.applyButton.longText=(Re)Apply filters"})
private class ApplyFiltersAction extends Action { private class ApplyFiltersAction extends Action {
@ -265,15 +184,24 @@ final public class FilterSetPanel extends BorderPane {
super(Bundle.FilterSetPanel_applyButton_text()); super(Bundle.FilterSetPanel_applyButton_text());
setLongText(Bundle.FilterSetPanel_applyButton_longText()); setLongText(Bundle.FilterSetPanel_applyButton_longText());
setGraphic(new ImageView(TICK)); setGraphic(new ImageView(TICK));
setEventHandler((ActionEvent t) -> { setEventHandler(actionEvent -> applyFilters());
applyFilters();
});
} }
} }
private void applyFilters() { @NbBundle.Messages({
Platform.runLater(() -> { "FilterSetPanel.hiddenDescriptionsListView.unhideAndRemove=Unhide and remove from list",
controller.pushFilters((RootFilter) filterTreeTable.getRoot().getValue()); "FilterSetPanel.hiddenDescriptionsListView.remove=Remove from list",})
}); private static class RemoveDescriptionFilterAction extends Action {
private static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS
RemoveDescriptionFilterAction(TimeLineController controller, Cell<DescriptionFilter> cell) {
super(actionEvent -> controller.getQuickHideFilters().remove(cell.getItem()));
setGraphic(new ImageView(SHOW));
textProperty().bind(
Bindings.when(cell.getItem().selectedProperty())
.then(Bundle.FilterSetPanel_hiddenDescriptionsListView_unhideAndRemove())
.otherwise(Bundle.FilterSetPanel_hiddenDescriptionsListView_remove()));
}
} }
} }

View File

@ -0,0 +1,104 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.sleuthkit.autopsy.timeline.ui.filtering;
import java.util.Arrays;
import java.util.function.BiPredicate;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableRow;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionGroup;
import org.controlsfx.control.action.ActionUtils;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.timeline.filters.Filter;
/**
*
*/
class FilterTreeTableRow extends TreeTableRow<Filter> {
@Override
protected void updateItem(Filter item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setContextMenu(null);
} else {
setContextMenu(ActionUtils.createContextMenu(Arrays.asList(new SelectActionsGroup(this))));
}
}
@NbBundle.Messages(value = {
"Timeline.ui.filtering.menuItem.select=select",
"Timeline.ui.filtering.menuItem.all=all",
"Timeline.ui.filtering.menuItem.none=none",
"Timeline.ui.filtering.menuItem.only=only",
"Timeline.ui.filtering.menuItem.others=others"})
private static enum SelectionAction {
ALL(Bundle.Timeline_ui_filtering_menuItem_all(),
(treeItem, row) -> true),
NONE(Bundle.Timeline_ui_filtering_menuItem_none(),
(treeItem, row) -> false),
ONLY(Bundle.Timeline_ui_filtering_menuItem_only(),
(treeItem, row) -> treeItem == row.getTreeItem()),
OTHER(Bundle.Timeline_ui_filtering_menuItem_others(),
(treeItem, row) -> treeItem != row.getTreeItem()),
SELECT(Bundle.Timeline_ui_filtering_menuItem_select(),
(treeItem, row) -> false == row.getItem().isSelected());
private final BiPredicate<TreeItem<Filter>, TreeTableRow<Filter>> selectionPredicate;
private final String displayName;
private SelectionAction(String displayName, BiPredicate<TreeItem<Filter>, TreeTableRow<Filter>> predicate) {
this.selectionPredicate = predicate;
this.displayName = displayName;
}
public void doSelection(TreeItem<Filter> treeItem, TreeTableRow<Filter> row) {
treeItem.getValue().setSelected(selectionPredicate.test(treeItem, row));
}
public String getDisplayName() {
return displayName;
}
}
private static final class SelectActionsGroup extends ActionGroup {
SelectActionsGroup(TreeTableRow<Filter> row) {
super(Bundle.Timeline_ui_filtering_menuItem_select(),
new Select(SelectionAction.ALL, row),
new Select(SelectionAction.NONE, row),
new Select(SelectionAction.ONLY, row),
new Select(SelectionAction.OTHER, row));
setEventHandler(new Select(SelectionAction.SELECT, row)::handle);
}
}
private static final class Select extends Action {
public TreeTableRow<Filter> getRow() {
return row;
}
private final TreeTableRow<Filter> row;
private final SelectionAction selectionAction;
Select(SelectionAction strategy, TreeTableRow<Filter> row) {
super(strategy.getDisplayName());
this.row = row;
this.selectionAction = strategy;
setEventHandler(actionEvent -> row.getTreeItem().getParent().getChildren().forEach(this::doSelection));
}
private void doSelection(TreeItem<Filter> treeItem) {
selectionAction.doSelection(treeItem, getRow());
}
}
}

View File

@ -0,0 +1,115 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.sleuthkit.autopsy.timeline.utils;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.TransformationList;
/**
*
*/
public class MappedList<E, F> extends TransformationList<E, F> {
private final Function<F, E> mapper;
public MappedList(ObservableList<? extends F> source, Function<F, E> mapper) {
super(source);
this.mapper = mapper;
}
@Override
public int getSourceIndex(int index) {
return index;
}
@Override
public E get(int index) {
return mapper.apply(getSource().get(index));
}
@Override
public int size() {
return getSource().size();
}
@Override
protected void sourceChanged(ListChangeListener.Change<? extends F> c) {
fireChange(new ListChangeListener.Change<E>(this) {
@Override
public boolean wasAdded() {
return c.wasAdded();
}
@Override
public boolean wasRemoved() {
return c.wasRemoved();
}
@Override
public boolean wasReplaced() {
return c.wasReplaced();
}
@Override
public boolean wasUpdated() {
return c.wasUpdated();
}
@Override
public boolean wasPermutated() {
return c.wasPermutated();
}
@Override
public int getPermutation(int i) {
return c.getPermutation(i);
}
@Override
protected int[] getPermutation() {
// This method is only called by the superclass methods
// wasPermutated() and getPermutation(int), which are
// both overriden by this class. There is no other way
// this method can be called.
throw new AssertionError("Unreachable code");
}
@Override
public List<E> getRemoved() {
ArrayList<E> res = new ArrayList<>(c.getRemovedSize());
for (F e : c.getRemoved()) {
res.add(mapper.apply(e));
}
return res;
}
@Override
public int getFrom() {
return c.getFrom();
}
@Override
public int getTo() {
return c.getTo();
}
@Override
public boolean next() {
return c.next();
}
@Override
public void reset() {
c.reset();
}
});
}
}

View File

@ -2,7 +2,7 @@
* *
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2012-2014 Basis Technology Corp. * Copyright 2011-2016 Basis Technology Corp.
* *
* Copyright 2012 42six Solutions. * Copyright 2012 42six Solutions.
* Contact: aebadirad <at> 42six <dot> com * Contact: aebadirad <at> 42six <dot> com
@ -63,10 +63,10 @@ import org.sleuthkit.datamodel.*;
class ExtractIE extends Extract { class ExtractIE extends Extract {
private static final Logger logger = Logger.getLogger(ExtractIE.class.getName()); private static final Logger logger = Logger.getLogger(ExtractIE.class.getName());
private IngestServices services = IngestServices.getInstance(); private final IngestServices services = IngestServices.getInstance();
private String moduleTempResultsDir; private final String moduleTempResultsDir;
private String PASCO_LIB_PATH; private String PASCO_LIB_PATH;
private String JAVA_PATH; private final String JAVA_PATH;
private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
private Content dataSource; private Content dataSource;
private IngestJobContext context; private IngestJobContext context;
@ -126,7 +126,7 @@ class ExtractIE extends Extract {
datetime = Long.valueOf(Tempdate); datetime = Long.valueOf(Tempdate);
String domain = Util.extractDomain(url); String domain = Util.extractDomain(url);
Collection<BlackboardAttribute> bbattributes = new ArrayList<BlackboardAttribute>(); Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL, bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL,
NbBundle.getMessage(this.getClass(), NbBundle.getMessage(this.getClass(),
"ExtractIE.parentModuleName.noSpace"), url)); "ExtractIE.parentModuleName.noSpace"), url));
@ -153,13 +153,15 @@ class ExtractIE extends Extract {
BufferedReader reader = new BufferedReader(new InputStreamReader(new ReadContentInputStream(fav))); BufferedReader reader = new BufferedReader(new InputStreamReader(new ReadContentInputStream(fav)));
String line, url = ""; String line, url = "";
try { try {
while ((line = reader.readLine()) != null) { line = reader.readLine();
while (null != line) {
// The actual shortcut line we are interested in is of the // The actual shortcut line we are interested in is of the
// form URL=http://path/to/website // form URL=http://path/to/website
if (line.startsWith("URL")) { //NON-NLS if (line.startsWith("URL")) { //NON-NLS
url = line.substring(line.indexOf("=") + 1); url = line.substring(line.indexOf("=") + 1);
break; break;
} }
line = reader.readLine();
} }
} catch (IOException ex) { } catch (IOException ex) {
logger.log(Level.WARNING, "Failed to read from content: " + fav.getName(), ex); //NON-NLS logger.log(Level.WARNING, "Failed to read from content: " + fav.getName(), ex); //NON-NLS
@ -231,7 +233,7 @@ class ExtractIE extends Extract {
datetime = Long.valueOf(tempDate); datetime = Long.valueOf(tempDate);
String domain = Util.extractDomain(url); String domain = Util.extractDomain(url);
Collection<BlackboardAttribute> bbattributes = new ArrayList<BlackboardAttribute>(); Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL, bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL,
NbBundle.getMessage(this.getClass(), NbBundle.getMessage(this.getClass(),
"ExtractIE.parentModuleName.noSpace"), url)); "ExtractIE.parentModuleName.noSpace"), url));
@ -377,6 +379,15 @@ class ExtractIE extends Extract {
ProcessBuilder processBuilder = new ProcessBuilder(commandLine); ProcessBuilder processBuilder = new ProcessBuilder(commandLine);
processBuilder.redirectOutput(new File(outputFileFullPath)); processBuilder.redirectOutput(new File(outputFileFullPath));
processBuilder.redirectError(new File(errFileFullPath)); processBuilder.redirectError(new File(errFileFullPath));
/*
* NOTE on Pasco return codes: There is no documentation for Pasco.
* Looking at the Pasco source code I see that when something goes
* wrong Pasco returns a negative number as a return code. However,
* we should still attempt to parse the Pasco output even if that
* happens. I have seen many situations where Pasco output file
* contains a lot of useful data and only the last entry is
* corrupted.
*/
ExecUtil.execute(processBuilder, new DataSourceIngestModuleProcessTerminator(context)); ExecUtil.execute(processBuilder, new DataSourceIngestModuleProcessTerminator(context));
// @@@ Investigate use of history versus cache as type. // @@@ Investigate use of history versus cache as type.
} catch (IOException ex) { } catch (IOException ex) {
@ -424,7 +435,7 @@ class ExtractIE extends Extract {
} }
// Keep a list of reported user accounts to avoid repeats // Keep a list of reported user accounts to avoid repeats
Set<String> reportedUserAccounts = new HashSet<String>(); Set<String> reportedUserAccounts = new HashSet<>();
while (fileScanner.hasNext()) { while (fileScanner.hasNext()) {
String line = fileScanner.nextLine(); String line = fileScanner.nextLine();
@ -439,12 +450,11 @@ class ExtractIE extends Extract {
continue; continue;
} }
String ddtime = lineBuff[2];
String actime = lineBuff[3]; String actime = lineBuff[3];
Long ftime = (long) 0; Long ftime = (long) 0;
String user = ""; String user;
String realurl = ""; String realurl;
String domain = ""; String domain;
/* /*
* We've seen two types of lines: URL http://XYZ.com .... URL * We've seen two types of lines: URL http://XYZ.com .... URL
@ -469,21 +479,15 @@ class ExtractIE extends Extract {
domain = Util.extractDomain(realurl); domain = Util.extractDomain(realurl);
if (!ddtime.isEmpty()) {
ddtime = ddtime.replace("T", " "); //NON-NLS
ddtime = ddtime.substring(ddtime.length() - 5);
}
if (!actime.isEmpty()) { if (!actime.isEmpty()) {
try { try {
Long epochtime = dateFormatter.parse(actime).getTime(); Long epochtime = dateFormatter.parse(actime).getTime();
ftime = epochtime.longValue(); ftime = epochtime / 1000;
ftime = ftime / 1000;
} catch (ParseException e) { } catch (ParseException e) {
this.addErrorMessage( this.addErrorMessage(
NbBundle.getMessage(this.getClass(), "ExtractIE.parsePascoOutput.errMsg.errParsingEntry", NbBundle.getMessage(this.getClass(), "ExtractIE.parsePascoOutput.errMsg.errParsingEntry",
this.getName())); this.getName()));
logger.log(Level.SEVERE, "Error parsing Pasco results.", e); //NON-NLS logger.log(Level.WARNING, String.format("Error parsing Pasco results, may have partial processing of corrupt file (id=%d)", origFile.getId()), e); //NON-NLS
} }
} }

View File

@ -220,7 +220,7 @@ public class RegressionTest extends TestCase {
JTableOperator jto = new JTableOperator(wo, 0); JTableOperator jto = new JTableOperator(wo, 0);
int row = jto.findCellRow("Hash Lookup", 1, 0); int row = jto.findCellRow("Hash Lookup", 1, 0);
jto.clickOnCell(row, 1); jto.clickOnCell(row, 1);
JButtonOperator jbo1 = new JButtonOperator(wo, "Advanced"); JButtonOperator jbo1 = new JButtonOperator(wo, "Global Settings");
jbo1.pushNoBlock(); jbo1.pushNoBlock();
} }
@ -260,7 +260,7 @@ public class RegressionTest extends TestCase {
JTableOperator jto = new JTableOperator(wo, 0); JTableOperator jto = new JTableOperator(wo, 0);
int row = jto.findCellRow("Keyword Search", 1, 0); int row = jto.findCellRow("Keyword Search", 1, 0);
jto.clickOnCell(row, 1); jto.clickOnCell(row, 1);
JButtonOperator jbo1 = new JButtonOperator(wo, "Advanced"); JButtonOperator jbo1 = new JButtonOperator(wo, "Global Settings");
jbo1.pushNoBlock(); jbo1.pushNoBlock();
} }

View File

@ -1,5 +1,5 @@
#Updated by build script #Updated by build script
#Wed, 06 Apr 2016 23:58:02 -0400 #Mon, 22 Feb 2016 16:37:47 -0500
LBL_splash_window_title=Starting Autopsy LBL_splash_window_title=Starting Autopsy
SPLASH_HEIGHT=314 SPLASH_HEIGHT=314
SPLASH_WIDTH=538 SPLASH_WIDTH=538

View File

@ -1,4 +1,4 @@
#Updated by build script #Updated by build script
#Wed, 06 Apr 2016 23:58:02 -0400 #Mon, 22 Feb 2016 16:37:47 -0500
CTL_MainWindow_Title=Autopsy 4.1.0 CTL_MainWindow_Title=Autopsy 4.1.0
CTL_MainWindow_Title_No_Project=Autopsy 4.1.0 CTL_MainWindow_Title_No_Project=Autopsy 4.1.0