Merge remote-tracking branch 'upstream/develop' into aut-2034
@ -604,7 +604,7 @@ public class Case implements SleuthkitCase.ErrorObserver {
|
||||
SleuthkitCase db;
|
||||
String caseDir;
|
||||
if (caseType == CaseType.SINGLE_USER_CASE) {
|
||||
String dbPath = metadata.getCaseDatabase(); //NON-NLS
|
||||
String dbPath = metadata.getCaseDatabasePath(); //NON-NLS
|
||||
db = SleuthkitCase.openCase(dbPath);
|
||||
caseDir = new File(dbPath).getParent();
|
||||
} else {
|
||||
@ -612,7 +612,7 @@ public class Case implements SleuthkitCase.ErrorObserver {
|
||||
throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.open.exception.multiUserCaseNotEnabled"));
|
||||
}
|
||||
try {
|
||||
db = SleuthkitCase.openCase(metadata.getCaseDatabase(), UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory());
|
||||
db = SleuthkitCase.openCase(metadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory());
|
||||
} catch (UserPreferencesException ex) {
|
||||
throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex);
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import org.sleuthkit.autopsy.coreutils.Version;
|
||||
import org.sleuthkit.autopsy.coreutils.XMLUtil;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
@ -53,22 +54,23 @@ public final class CaseMetadata {
|
||||
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 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 TEXT_INDEX_NAME_ELEMENT = "TextIndexName"; //NON-NLS
|
||||
private static final String CURRENT_SCHEMA_VERSION = "2.0";
|
||||
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 CREATED_DATE_ELEMENT_NAME = "CreatedDate"; //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 SAVED_BY_BUILD_ELEMENT_NAME = "SavedByBuild"; //NON-NLS
|
||||
private final static String AUTOPSY_CREATED_BY_ELEMENT_NAME = "CreatedByAutopsyVersion"; //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_NAME_ELEMENT_NAME = "Name"; //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 CASE_TYPE_ELEMENT_NAME = "CaseType"; //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 Case.CaseType caseType;
|
||||
private String caseName;
|
||||
@ -77,7 +79,7 @@ public final class CaseMetadata {
|
||||
private String caseDatabase;
|
||||
private String textIndexName;
|
||||
private String createdDate;
|
||||
private String createdByBuild;
|
||||
private String createdByVersion;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* a new case metadata file.
|
||||
* a new case metadata file that is created using the supplied metadata.
|
||||
*
|
||||
* @param caseDirectory The case directory.
|
||||
* @param caseType The type of case.
|
||||
@ -113,7 +115,7 @@ public final class CaseMetadata {
|
||||
this.examiner = examiner;
|
||||
this.caseDatabase = caseDatabase;
|
||||
this.textIndexName = caseTextIndexName;
|
||||
createdByBuild = System.getProperty("netbeans.buildnumber");
|
||||
createdByVersion = Version.getVersion();
|
||||
createdDate = CaseMetadata.DATE_FORMAT.format(new Date());
|
||||
writeToFile();
|
||||
}
|
||||
@ -147,7 +149,7 @@ public final class CaseMetadata {
|
||||
* @return The case directory.
|
||||
*/
|
||||
public String getCaseDirectory() {
|
||||
return this.metadataFilePath.getParent().toString();
|
||||
return metadataFilePath.getParent().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,7 +158,7 @@ public final class CaseMetadata {
|
||||
* @return The case type.
|
||||
*/
|
||||
public Case.CaseType getCaseType() {
|
||||
return this.caseType;
|
||||
return caseType;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -175,10 +177,10 @@ public final class CaseMetadata {
|
||||
* @param caseName A case display name.
|
||||
*/
|
||||
void setCaseName(String caseName) throws CaseMetadataException {
|
||||
String oldCaseName = this.caseName;
|
||||
String oldCaseName = caseName;
|
||||
this.caseName = caseName;
|
||||
try {
|
||||
this.writeToFile();
|
||||
writeToFile();
|
||||
} catch (CaseMetadataException ex) {
|
||||
this.caseName = oldCaseName;
|
||||
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.
|
||||
* For a multi-user case, the case database name.
|
||||
* @return The case database name.
|
||||
*/
|
||||
public String getCaseDatabase() {
|
||||
return caseDatabase;
|
||||
public String getCaseDatabaseName() {
|
||||
if (caseType == Case.CaseType.MULTI_USER_CASE) {
|
||||
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
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
void setCreatedDate(String createdDate) throws CaseMetadataException {
|
||||
String oldCreatedDate = this.createdDate;
|
||||
String oldCreatedDate = createdDate;
|
||||
this.createdDate = createdDate;
|
||||
try {
|
||||
this.writeToFile();
|
||||
writeToFile();
|
||||
} catch (CaseMetadataException ex) {
|
||||
this.createdDate = oldCreatedDate;
|
||||
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.
|
||||
*/
|
||||
String getCreatedByBuild() {
|
||||
return createdByBuild;
|
||||
String getCreatedByVersion() {
|
||||
return createdByVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the build that created the case. Used for preserving the case
|
||||
* creation build during single-user to multi-user case conversion.
|
||||
* Sets the Autopsy version that created the case. Used for preserving this
|
||||
* metadata during single-user to multi-user case conversion.
|
||||
*
|
||||
* @param buildVersion An build version identifier.
|
||||
*/
|
||||
void setCreatedByBuild(String buildVersion) throws CaseMetadataException {
|
||||
String oldCreatedByVersion = this.createdByBuild;
|
||||
this.createdByBuild = buildVersion;
|
||||
void setCreatedByVersion(String buildVersion) throws CaseMetadataException {
|
||||
String oldCreatedByVersion = this.createdByVersion;
|
||||
this.createdByVersion = buildVersion;
|
||||
try {
|
||||
this.writeToFile();
|
||||
} catch (CaseMetadataException ex) {
|
||||
this.createdByBuild = oldCreatedByVersion;
|
||||
this.createdByVersion = oldCreatedByVersion;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
@ -323,22 +344,22 @@ public final class CaseMetadata {
|
||||
Element rootElement = doc.createElement(ROOT_ELEMENT_NAME);
|
||||
doc.appendChild(rootElement);
|
||||
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, CREATED_BY_BUILD_ELEMENT_NAME, this.getCreatedByBuild());
|
||||
createChildElement(doc, rootElement, SAVED_BY_BUILD_ELEMENT_NAME, System.getProperty("netbeans.buildnumber"));
|
||||
createChildElement(doc, rootElement, AUTOPSY_CREATED_BY_ELEMENT_NAME, createdByVersion);
|
||||
createChildElement(doc, rootElement, AUTOPSY_SAVED_BY_ELEMENT_NAME, Version.getVersion());
|
||||
Element caseElement = doc.createElement(CASE_ELEMENT_NAME);
|
||||
rootElement.appendChild(caseElement);
|
||||
|
||||
/*
|
||||
* Create the children of the case element.
|
||||
*/
|
||||
createChildElement(doc, caseElement, CASE_NAME_ELEMENT_NAME, this.getCaseName());
|
||||
createChildElement(doc, caseElement, CASE_NUMBER_ELEMENT_NAME, this.getCaseNumber());
|
||||
createChildElement(doc, caseElement, EXAMINER_ELEMENT_NAME, this.getExaminer());
|
||||
createChildElement(doc, caseElement, CASE_TYPE_ELEMENT_NAME, this.getCaseType().toString());
|
||||
createChildElement(doc, caseElement, CASE_DATABASE_ELEMENT_NAME, this.getCaseDatabaseName());
|
||||
createChildElement(doc, caseElement, TEXT_INDEX_NAME_ELEMENT, this.getTextIndexName());
|
||||
createChildElement(doc, caseElement, CASE_NAME_ELEMENT_NAME, caseName);
|
||||
createChildElement(doc, caseElement, CASE_NUMBER_ELEMENT_NAME, caseNumber);
|
||||
createChildElement(doc, caseElement, EXAMINER_ELEMENT_NAME, examiner);
|
||||
createChildElement(doc, caseElement, CASE_TYPE_ELEMENT_NAME, caseType.toString());
|
||||
createChildElement(doc, caseElement, CASE_DATABASE_ELEMENT_NAME, caseDatabase);
|
||||
createChildElement(doc, caseElement, TEXT_INDEX_ELEMENT, textIndexName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -381,9 +402,9 @@ public final class CaseMetadata {
|
||||
String schemaVersion = getElementTextContent(rootElement, SCHEMA_VERSION_ELEMENT_NAME, true);
|
||||
this.createdDate = getElementTextContent(rootElement, CREATED_DATE_ELEMENT_NAME, true);
|
||||
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 {
|
||||
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)) {
|
||||
this.caseDatabase = getElementTextContent(caseElement, CASE_DATABASE_NAME_ELEMENT_NAME, true);
|
||||
this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_NAME_ELEMENT, true);
|
||||
} else {
|
||||
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.
|
||||
@ -414,7 +436,7 @@ public final class CaseMetadata {
|
||||
if (!schemaVersion.equals(CURRENT_SCHEMA_VERSION)) {
|
||||
writeToFile();
|
||||
}
|
||||
|
||||
|
||||
} catch (ParserConfigurationException | SAXException | IOException ex) {
|
||||
throw new CaseMetadataException(String.format("Error reading from case metadata file %s", metadataFilePath), ex);
|
||||
}
|
||||
@ -446,7 +468,7 @@ public final class CaseMetadata {
|
||||
|
||||
/**
|
||||
* Exception thrown by the CaseMetadata class when there is a problem
|
||||
* accessing the metadata for a case.
|
||||
* accessing the metadata for a case.
|
||||
*/
|
||||
public final static class CaseMetadataException extends Exception {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -88,7 +88,11 @@ class CasePropertiesForm extends javax.swing.JPanel {
|
||||
current = currentCase;
|
||||
|
||||
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();
|
||||
tbDbType.setText(caseType.getLocalizedDisplayName());
|
||||
if (caseType == Case.CaseType.SINGLE_USER_CASE) {
|
||||
|
@ -198,7 +198,7 @@ public class SingleUserCaseConverter {
|
||||
dbName, solrName);
|
||||
// Set created date. This calls writefile, no need to call it again
|
||||
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
|
||||
// (if requested). This *should* be fairly safe - at this point we know there was an autopsy file
|
||||
|
@ -47,6 +47,7 @@ import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.concurrent.Worker;
|
||||
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.ingest.IngestManager;
|
||||
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.db.EventsRepository;
|
||||
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
|
||||
@ -290,6 +292,21 @@ public class TimeLineController {
|
||||
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
|
||||
* reference to {@link EventsRepository#rebuildRepository(java.util.function.Consumer)
|
||||
|
@ -28,14 +28,18 @@ import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
private FilteredEventsModel eventsModel;
|
||||
|
||||
@NbBundle.Messages({"ResetFilters.text=Reset all filters",
|
||||
"RestFilters.longText=Reset all filters to their default state."})
|
||||
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());
|
||||
eventsModel = controller.getEventsModel();
|
||||
disabledProperty().bind(new BooleanBinding() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2013-15 Basis Technology Corp.
|
||||
* Copyright 2013-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -33,12 +33,12 @@ import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
|
||||
|
||||
/**
|
||||
* Represents a set of other (TimeLineEvent) events clustered together. All the
|
||||
* sub events should have the same type and matching descriptions at the
|
||||
* designated 'zoom level', and be 'close together' in time.
|
||||
* Represents a set of other events clustered together. All the sub events
|
||||
* should have the same type and matching descriptions at the designated "zoom
|
||||
* level", and be "close together" in time.
|
||||
*/
|
||||
@Immutable
|
||||
public class EventCluster implements EventBundle<EventStripe> {
|
||||
public class EventCluster implements MultiEvent<EventStripe> {
|
||||
|
||||
/**
|
||||
* 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())) {
|
||||
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> hashHitsUnion = Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits());
|
||||
Sets.SetView<Long> taggedUnion = Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags());
|
||||
Sets.SetView<Long> idsUnion =
|
||||
Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs());
|
||||
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;
|
||||
@ -103,7 +108,9 @@ public class EventCluster implements EventBundle<EventStripe> {
|
||||
*/
|
||||
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.type = type;
|
||||
@ -115,12 +122,13 @@ public class EventCluster implements EventBundle<EventStripe> {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<EventStripe> getParentBundle() {
|
||||
public Optional<EventStripe> getParent() {
|
||||
return Optional.ofNullable(parent);
|
||||
}
|
||||
|
||||
@ -139,19 +147,16 @@ public class EventCluster implements EventBundle<EventStripe> {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReturnOfCollectionOrArrayField")
|
||||
public ImmutableSet<Long> getEventIDs() {
|
||||
return eventIDs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReturnOfCollectionOrArrayField")
|
||||
public ImmutableSet<Long> getEventIDsWithHashHits() {
|
||||
return hashHits;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReturnOfCollectionOrArrayField")
|
||||
public ImmutableSet<Long> getEventIDsWithTags() {
|
||||
return tagged;
|
||||
}
|
||||
@ -181,15 +186,53 @@ public class EventCluster implements EventBundle<EventStripe> {
|
||||
* EventBundle as the 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SortedSet< EventCluster> getClusters() {
|
||||
public SortedSet<EventCluster> getClusters() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2015 Basis Technology Corp.
|
||||
* Copyright 2015-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -22,10 +22,10 @@ import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.SortedSet;
|
||||
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.zooming.DescriptionLoD;
|
||||
|
||||
@ -34,15 +34,15 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
|
||||
* description, and zoom levels, but not necessarily close together in time.
|
||||
*/
|
||||
@Immutable
|
||||
public final class EventStripe implements EventBundle<EventCluster> {
|
||||
public final class EventStripe implements MultiEvent<EventCluster> {
|
||||
|
||||
public static EventStripe merge(EventStripe u, EventStripe v) {
|
||||
Preconditions.checkNotNull(u);
|
||||
Preconditions.checkNotNull(v);
|
||||
Preconditions.checkArgument(Objects.equal(u.description, v.description));
|
||||
Preconditions.checkArgument(Objects.equal(u.lod, v.lod));
|
||||
Preconditions.checkArgument(Objects.equal(u.type, v.type));
|
||||
Preconditions.checkArgument(Objects.equal(u.parent, v.parent));
|
||||
Preconditions.checkArgument(Objects.equals(u.description, v.description));
|
||||
Preconditions.checkArgument(Objects.equals(u.lod, v.lod));
|
||||
Preconditions.checkArgument(Objects.equals(u.type, v.type));
|
||||
Preconditions.checkArgument(Objects.equals(u.parent, v.parent));
|
||||
return new EventStripe(u, v);
|
||||
}
|
||||
|
||||
@ -82,8 +82,10 @@ public final class EventStripe implements EventBundle<EventCluster> {
|
||||
private final ImmutableSet<Long> hashHits;
|
||||
|
||||
public EventStripe withParent(EventCluster parent) {
|
||||
EventStripe eventStripe = new EventStripe(parent, this.type, this.description, this.lod, clusters, eventIDs, tagged, hashHits);
|
||||
return eventStripe;
|
||||
if (java.util.Objects.nonNull(this.parent)) {
|
||||
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) {
|
||||
@ -98,9 +100,10 @@ public final class EventStripe implements EventBundle<EventCluster> {
|
||||
this.hashHits = hashHits;
|
||||
}
|
||||
|
||||
public EventStripe(EventCluster cluster, EventCluster parent) {
|
||||
public EventStripe(EventCluster cluster) {
|
||||
|
||||
this.clusters = ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis))
|
||||
.add(cluster).build();
|
||||
.add(cluster.withParent(this)).build();
|
||||
|
||||
type = cluster.getEventType();
|
||||
description = cluster.getDescription();
|
||||
@ -108,7 +111,7 @@ public final class EventStripe implements EventBundle<EventCluster> {
|
||||
eventIDs = cluster.getEventIDs();
|
||||
tagged = cluster.getEventIDsWithTags();
|
||||
hashHits = cluster.getEventIDsWithHashHits();
|
||||
this.parent = parent;
|
||||
this.parent = null;
|
||||
}
|
||||
|
||||
private EventStripe(EventStripe u, EventStripe v) {
|
||||
@ -132,14 +135,22 @@ public final class EventStripe implements EventBundle<EventCluster> {
|
||||
.addAll(u.getEventIDsWithHashHits())
|
||||
.addAll(v.getEventIDsWithHashHits())
|
||||
.build();
|
||||
parent = u.getParentBundle().orElse(v.getParentBundle().orElse(null));
|
||||
parent = u.getParent().orElse(v.getParent().orElse(null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<EventCluster> getParentBundle() {
|
||||
public Optional<EventCluster> getParent() {
|
||||
return Optional.ofNullable(parent);
|
||||
}
|
||||
|
||||
public Optional<EventStripe> getParentStripe() {
|
||||
if (getParent().isPresent()) {
|
||||
return getParent().get().getParent();
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
@ -156,19 +167,16 @@ public final class EventStripe implements EventBundle<EventCluster> {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReturnOfCollectionOrArrayField")
|
||||
public ImmutableSet<Long> getEventIDs() {
|
||||
return eventIDs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReturnOfCollectionOrArrayField")
|
||||
public ImmutableSet<Long> getEventIDsWithHashHits() {
|
||||
return hashHits;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReturnOfCollectionOrArrayField")
|
||||
public ImmutableSet<Long> getEventIDsWithTags() {
|
||||
return tagged;
|
||||
}
|
||||
@ -184,13 +192,53 @@ public final class EventStripe implements EventBundle<EventCluster> {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReturnOfCollectionOrArrayField")
|
||||
public ImmutableSortedSet< EventCluster> getClusters() {
|
||||
return clusters;
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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.db.EventsRepository;
|
||||
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.DataSourcesFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.Filter;
|
||||
@ -232,11 +233,11 @@ public final class FilteredEventsModel {
|
||||
return repo.getBoundingEventsInterval(zoomParametersProperty().get().getTimeRange(), zoomParametersProperty().get().getFilter());
|
||||
}
|
||||
|
||||
public TimeLineEvent getEventById(Long eventID) {
|
||||
public SingleEvent getEventById(Long eventID) {
|
||||
return repo.getEventById(eventID);
|
||||
}
|
||||
|
||||
public Set<TimeLineEvent> getEventsById(Collection<Long> eventIDs) {
|
||||
public Set<SingleEvent> getEventsById(Collection<Long> eventIDs) {
|
||||
return repo.getEventsById(eventIDs);
|
||||
}
|
||||
|
||||
@ -351,14 +352,14 @@ public final class FilteredEventsModel {
|
||||
ContentTag contentTag = evt.getAddedTag();
|
||||
Content content = contentTag.getContent();
|
||||
Set<Long> updatedEventIDs = repo.addTag(content.getId(), null, contentTag, null);
|
||||
return postTagsUpdated(updatedEventIDs);
|
||||
return postTagsAdded(updatedEventIDs);
|
||||
}
|
||||
|
||||
synchronized public boolean handleArtifactTagAdded(BlackBoardArtifactTagAddedEvent evt) {
|
||||
BlackboardArtifactTag artifactTag = evt.getAddedTag();
|
||||
BlackboardArtifact artifact = artifactTag.getArtifact();
|
||||
Set<Long> updatedEventIDs = repo.addTag(artifact.getObjectID(), artifact.getArtifactID(), artifactTag, null);
|
||||
return postTagsUpdated(updatedEventIDs);
|
||||
return postTagsAdded(updatedEventIDs);
|
||||
}
|
||||
|
||||
synchronized public boolean handleContentTagDeleted(ContentTagDeletedEvent evt) {
|
||||
@ -367,7 +368,7 @@ public final class FilteredEventsModel {
|
||||
Content content = autoCase.getSleuthkitCase().getContentById(deletedTagInfo.getContentID());
|
||||
boolean tagged = autoCase.getServices().getTagsManager().getContentTagsByContent(content).isEmpty() == false;
|
||||
Set<Long> updatedEventIDs = repo.deleteTag(content.getId(), null, deletedTagInfo.getTagID(), tagged);
|
||||
return postTagsUpdated(updatedEventIDs);
|
||||
return postTagsDeleted(updatedEventIDs);
|
||||
} catch (TskCoreException ex) {
|
||||
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());
|
||||
boolean tagged = autoCase.getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact).isEmpty() == false;
|
||||
Set<Long> updatedEventIDs = repo.deleteTag(artifact.getObjectID(), artifact.getArtifactID(), deletedTagInfo.getTagID(), tagged);
|
||||
return postTagsUpdated(updatedEventIDs);
|
||||
return postTagsDeleted(updatedEventIDs);
|
||||
} catch (TskCoreException ex) {
|
||||
LOGGER.log(Level.SEVERE, "unable to determine tagged status of artifact.", ex); //NON-NLS
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean postTagsUpdated(Set<Long> updatedEventIDs) {
|
||||
private boolean postTagsAdded(Set<Long> updatedEventIDs) {
|
||||
boolean tagsUpdated = !updatedEventIDs.isEmpty();
|
||||
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;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2015 Basis Technology Corp.
|
||||
* Copyright 2015-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -19,37 +19,16 @@
|
||||
package org.sleuthkit.autopsy.timeline.datamodel;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
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.
|
||||
*/
|
||||
public interface EventBundle<ParentType extends EventBundle<?>> {
|
||||
|
||||
String getDescription();
|
||||
|
||||
DescriptionLoD getDescriptionLoD();
|
||||
|
||||
Set<Long> getEventIDs();
|
||||
|
||||
Set<Long> getEventIDsWithHashHits();
|
||||
|
||||
Set<Long> getEventIDsWithTags();
|
||||
|
||||
EventType getEventType();
|
||||
public interface MultiEvent<ParentType extends MultiEvent<?>> extends TimeLineEvent {
|
||||
|
||||
long getEndMillis();
|
||||
|
||||
long getStartMillis();
|
||||
|
||||
Optional<ParentType> getParentBundle();
|
||||
|
||||
default int getCount() {
|
||||
return getEventIDs().size();
|
||||
}
|
||||
Optional<ParentType> getParent();
|
||||
|
||||
SortedSet<EventCluster> getClusters();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2014-15 Basis Technology Corp.
|
||||
* Copyright 2016 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -18,101 +18,35 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.datamodel;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
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 TimeLineEvent {
|
||||
public interface TimeLineEvent {
|
||||
|
||||
private final long eventID;
|
||||
private final long fileID;
|
||||
private final Long artifactID;
|
||||
private final long dataSourceID;
|
||||
public String getDescription();
|
||||
|
||||
private final long time;
|
||||
private final EventType subType;
|
||||
private final ImmutableMap<DescriptionLoD, String> descriptions;
|
||||
public DescriptionLoD getDescriptionLoD();
|
||||
|
||||
private final TskData.FileKnown known;
|
||||
private final boolean hashHit;
|
||||
private final boolean tagged;
|
||||
Set<Long> getEventIDs();
|
||||
|
||||
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) {
|
||||
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);
|
||||
Set<Long> getEventIDsWithHashHits();
|
||||
|
||||
this.known = known;
|
||||
this.hashHit = hashHit;
|
||||
this.tagged = tagged;
|
||||
this.dataSourceID = dataSourceID;
|
||||
Set<Long> getEventIDsWithTags();
|
||||
|
||||
EventType getEventType();
|
||||
|
||||
long getEndMillis();
|
||||
|
||||
long getStartMillis();
|
||||
|
||||
default int getSize() {
|
||||
return getEventIDs().size();
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
SortedSet<EventCluster> getClusters();
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ import org.sleuthkit.autopsy.coreutils.Version;
|
||||
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.TimeLineEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.BaseTypes;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType;
|
||||
@ -323,8 +323,8 @@ public class EventDB {
|
||||
return null;
|
||||
}
|
||||
|
||||
TimeLineEvent getEventById(Long eventID) {
|
||||
TimeLineEvent result = null;
|
||||
SingleEvent getEventById(Long eventID) {
|
||||
SingleEvent result = null;
|
||||
DBLock.lock();
|
||||
try {
|
||||
getEventByIDStmt.clearParameters();
|
||||
@ -939,8 +939,8 @@ public class EventDB {
|
||||
}
|
||||
}
|
||||
|
||||
private TimeLineEvent constructTimeLineEvent(ResultSet rs) throws SQLException {
|
||||
return new TimeLineEvent(rs.getLong("event_id"), //NON-NLS
|
||||
private SingleEvent constructTimeLineEvent(ResultSet rs) throws SQLException {
|
||||
return new SingleEvent(rs.getLong("event_id"), //NON-NLS
|
||||
rs.getLong("datasource_id"), //NON-NLS
|
||||
rs.getLong("file_id"), //NON-NLS
|
||||
rs.getLong("artifact_id"), //NON-NLS
|
||||
@ -1050,7 +1050,7 @@ public class EventDB {
|
||||
|
||||
switch (Version.getBuildType()) {
|
||||
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;
|
||||
case RELEASE:
|
||||
default:
|
||||
@ -1097,8 +1097,7 @@ public class EventDB {
|
||||
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
|
||||
|
||||
return new EventCluster(interval, type, eventIDs, hashHits, tagged,
|
||||
description, descriptionLOD);
|
||||
return new EventCluster(interval, type, eventIDs, hashHits, tagged, description, descriptionLOD);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1159,7 +1158,7 @@ public class EventDB {
|
||||
|
||||
for (EventCluster eventCluster : aggEvents) {
|
||||
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());
|
||||
|
@ -59,7 +59,7 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined;
|
||||
import org.sleuthkit.autopsy.timeline.CancellationProgressTask;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
|
||||
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.EventType;
|
||||
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> 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, List<EventStripe>> eventStripeCache;
|
||||
|
||||
@ -179,11 +179,11 @@ public class EventsRepository {
|
||||
|
||||
}
|
||||
|
||||
public TimeLineEvent getEventById(Long eventID) {
|
||||
public SingleEvent getEventById(Long eventID) {
|
||||
return idToEventCache.getUnchecked(eventID);
|
||||
}
|
||||
|
||||
synchronized public Set<TimeLineEvent> getEventsById(Collection<Long> eventIDs) {
|
||||
synchronized public Set<SingleEvent> getEventsById(Collection<Long> eventIDs) {
|
||||
return eventIDs.stream()
|
||||
.map(idToEventCache::getUnchecked)
|
||||
.collect(Collectors.toSet());
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -26,16 +26,14 @@ import java.util.Set;
|
||||
* been(un)tagged. This event is not intended for use out side of the timeline
|
||||
* module.
|
||||
*/
|
||||
public class TagsUpdatedEvent {
|
||||
abstract public class TagsUpdatedEvent {
|
||||
|
||||
private final Set<Long> updatedEventIDs;
|
||||
|
||||
|
||||
public ImmutableSet<Long> getUpdatedEventIDs() {
|
||||
return ImmutableSet.copyOf(updatedEventIDs);
|
||||
}
|
||||
|
||||
|
||||
public TagsUpdatedEvent(Set<Long> updatedEventIDs) {
|
||||
this.updatedEventIDs = updatedEventIDs;
|
||||
|
||||
|
@ -37,30 +37,30 @@ import org.sleuthkit.autopsy.datamodel.DisplayableItemNode;
|
||||
import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor;
|
||||
import org.sleuthkit.autopsy.datamodel.NodeProperty;
|
||||
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.BlackboardArtifact;
|
||||
import org.sleuthkit.datamodel.Content;
|
||||
|
||||
/**
|
||||
* * Explorer Node for {@link TimeLineEvent}s.
|
||||
* * Explorer Node for {@link SingleEvent}s.
|
||||
*/
|
||||
class EventNode extends DisplayableItemNode {
|
||||
|
||||
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));
|
||||
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));
|
||||
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
|
||||
@ -85,15 +85,15 @@ class EventNode extends DisplayableItemNode {
|
||||
properties.put(new NodeProperty<>("icon", "Icon", "icon", true)); // NON-NLS //gets overridden with icon
|
||||
properties.put(timePropery);
|
||||
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<>("eventSubType", "Sub Type", "sub type", e.getType().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.getEventType().getDisplayName())); // NON-NLS
|
||||
properties.put(new NodeProperty<>("Known", "Known", "known", e.getKnown().toString())); // NON-NLS
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -32,7 +32,7 @@ import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.datamodel.DisplayableItemNode;
|
||||
import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor;
|
||||
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.datamodel.AbstractFile;
|
||||
import org.sleuthkit.datamodel.BlackboardArtifact;
|
||||
@ -111,11 +111,11 @@ public class EventRootNode extends DisplayableItemNode {
|
||||
@Override
|
||||
protected Node createNodeForKey(Long eventID) {
|
||||
if (eventID >= 0) {
|
||||
final TimeLineEvent eventById = filteredEvents.getEventById(eventID);
|
||||
final SingleEvent eventById = filteredEvents.getEventById(eventID);
|
||||
try {
|
||||
AbstractFile file = Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(eventById.getFileID());
|
||||
if (file != null) {
|
||||
if (eventById.getType().getSuperType() == BaseTypes.FILE_SYSTEM) {
|
||||
if (eventById.getEventType().getSuperType() == BaseTypes.FILE_SYSTEM) {
|
||||
return new EventNode(eventById, file);
|
||||
} else {
|
||||
BlackboardArtifact blackboardArtifact = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifact(eventById.getArtifactID());
|
||||
|
@ -60,7 +60,7 @@ public abstract class AbstractFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public boolean isDisabled() {
|
||||
return disabled.get();
|
||||
return disabledProperty().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -64,8 +64,8 @@ public abstract class CompoundFilter<SubFilterType extends Filter> extends Abstr
|
||||
}
|
||||
});
|
||||
this.subFilters.setAll(subFilters);
|
||||
|
||||
this.selectedProperty().addListener(activeProperty -> {
|
||||
|
||||
this.selectedProperty().addListener(activeProperty -> {
|
||||
getSubFilters().forEach(subFilter -> subFilter.setDisabled(isActive() == false));
|
||||
});
|
||||
this.disabledProperty().addListener(activeProperty -> {
|
||||
|
@ -18,7 +18,7 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.filters;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
@ -39,9 +39,9 @@ public class DataSourcesFilter extends UnionFilter<DataSourceFilter> {
|
||||
filterCopy.setSelected(isSelected());
|
||||
filterCopy.setDisabled(isDisabled());
|
||||
//add a copy of each subfilter
|
||||
this.getSubFilters().forEach((DataSourceFilter t) -> {
|
||||
filterCopy.addSubFilter(t.copyOf());
|
||||
});
|
||||
getSubFilters().forEach(dataSourceFilter ->
|
||||
filterCopy.addSubFilter(dataSourceFilter.copyOf())
|
||||
);
|
||||
|
||||
return filterCopy;
|
||||
}
|
||||
@ -66,13 +66,7 @@ public class DataSourcesFilter extends UnionFilter<DataSourceFilter> {
|
||||
}
|
||||
|
||||
public void addSubFilter(DataSourceFilter dataSourceFilter) {
|
||||
if (getSubFilters().stream().map(DataSourceFilter.class::cast)
|
||||
.map(DataSourceFilter::getDataSourceID)
|
||||
.filter(t -> t == dataSourceFilter.getDataSourceID())
|
||||
.findAny().isPresent() == false) {
|
||||
getSubFilters().add(dataSourceFilter);
|
||||
getSubFilters().sort(Comparator.comparing(DataSourceFilter::getDisplayName));
|
||||
}
|
||||
super.addSubFilter(dataSourceFilter);
|
||||
if (getSubFilters().size() > 1) {
|
||||
setSelected(Boolean.TRUE);
|
||||
}
|
||||
@ -106,4 +100,8 @@ public class DataSourcesFilter extends UnionFilter<DataSourceFilter> {
|
||||
return Bindings.or(super.disabledProperty(), Bindings.size(getSubFilters()).lessThanOrEqualTo(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
Predicate<DataSourceFilter> getDuplicatePredicate(DataSourceFilter subfilter) {
|
||||
return dataSourcefilter -> dataSourcefilter.getDataSourceID() == subfilter.getDataSourceID();
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.filters;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
@ -45,9 +45,9 @@ public class HashHitsFilter extends UnionFilter<HashSetFilter> {
|
||||
filterCopy.setSelected(isSelected());
|
||||
filterCopy.setDisabled(isDisabled());
|
||||
//add a copy of each subfilter
|
||||
this.getSubFilters().forEach((HashSetFilter t) -> {
|
||||
filterCopy.addSubFilter(t.copyOf());
|
||||
});
|
||||
this.getSubFilters().forEach(hashSetFilter ->
|
||||
filterCopy.addSubFilter(hashSetFilter.copyOf())
|
||||
);
|
||||
return filterCopy;
|
||||
}
|
||||
|
||||
@ -86,18 +86,13 @@ public class HashHitsFilter extends UnionFilter<HashSetFilter> {
|
||||
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
|
||||
public ObservableBooleanValue disabledProperty() {
|
||||
return Bindings.or(super.disabledProperty(), Bindings.isEmpty(getSubFilters()));
|
||||
}
|
||||
|
||||
@Override
|
||||
Predicate<HashSetFilter> getDuplicatePredicate(HashSetFilter subfilter) {
|
||||
return hashSetFilter -> subfilter.getHashSetID() == hashSetFilter.getHashSetID();
|
||||
}
|
||||
}
|
||||
|
@ -84,5 +84,4 @@ public class HashSetFilter extends AbstractFilter {
|
||||
}
|
||||
return isSelected() == other.isSelected();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,6 +19,7 @@
|
||||
package org.sleuthkit.autopsy.timeline.filters;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
@ -46,9 +47,9 @@ public class TagsFilter extends UnionFilter<TagNameFilter> {
|
||||
filterCopy.setSelected(isSelected());
|
||||
filterCopy.setDisabled(isDisabled());
|
||||
//add a copy of each subfilter
|
||||
this.getSubFilters().forEach((TagNameFilter t) -> {
|
||||
filterCopy.addSubFilter(t.copyOf());
|
||||
});
|
||||
getSubFilters().forEach(tagNameFilter ->
|
||||
filterCopy.addSubFilter(tagNameFilter.copyOf())
|
||||
);
|
||||
return filterCopy;
|
||||
}
|
||||
|
||||
@ -87,17 +88,6 @@ public class TagsFilter extends UnionFilter<TagNameFilter> {
|
||||
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) {
|
||||
getSubFilters().removeIf(subfilter -> subfilter.getTagName().equals(tagName));
|
||||
getSubFilters().sort(Comparator.comparing(TagNameFilter::getDisplayName));
|
||||
@ -107,4 +97,10 @@ public class TagsFilter extends UnionFilter<TagNameFilter> {
|
||||
public ObservableBooleanValue disabledProperty() {
|
||||
return Bindings.or(super.disabledProperty(), Bindings.isEmpty(getSubFilters()));
|
||||
}
|
||||
|
||||
@Override
|
||||
Predicate<TagNameFilter> getDuplicatePredicate(TagNameFilter subfilter) {
|
||||
return tagNameFilter -> subfilter.getTagName().equals(tagNameFilter.getTagName());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,7 +18,9 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.filters;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.image.Image;
|
||||
@ -33,6 +35,8 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType;
|
||||
*/
|
||||
public class TypeFilter extends UnionFilter<TypeFilter> {
|
||||
|
||||
static private final Comparator<TypeFilter> comparator = Comparator.comparing(TypeFilter::getEventType, EventType.getComparator());
|
||||
|
||||
/**
|
||||
* the event type this filter passes
|
||||
*/
|
||||
@ -52,7 +56,7 @@ public class TypeFilter extends UnionFilter<TypeFilter> {
|
||||
|
||||
if (recursive) { // add subfilters for each subtype
|
||||
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
|
||||
public TypeFilter copyOf() {
|
||||
//make a nonrecursive copy of this filter
|
||||
final TypeFilter typeFilter = new TypeFilter(eventType, false);
|
||||
typeFilter.setSelected(isSelected());
|
||||
typeFilter.setDisabled(isDisabled());
|
||||
final TypeFilter filterCopy = new TypeFilter(eventType, false);
|
||||
filterCopy.setSelected(isSelected());
|
||||
filterCopy.setDisabled(isDisabled());
|
||||
//add a copy of each subfilter
|
||||
this.getSubFilters().forEach((TypeFilter t) -> {
|
||||
typeFilter.getSubFilters().add(t.copyOf());
|
||||
});
|
||||
getSubFilters().forEach(typeFilter ->
|
||||
filterCopy.addSubFilter(typeFilter.copyOf(), comparator)
|
||||
);
|
||||
|
||||
return typeFilter;
|
||||
return filterCopy;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -142,4 +146,9 @@ public class TypeFilter extends UnionFilter<TypeFilter> {
|
||||
hash = 67 * hash + Objects.hashCode(this.eventType);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
Predicate<TypeFilter> getDuplicatePredicate(TypeFilter subfilter) {
|
||||
return t -> subfilter.getEventType().equals(t.eventType);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.filters;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Predicate;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
@ -34,5 +36,17 @@ abstract public class UnionFilter<SubFilterType extends Filter> extends Compound
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
BIN
Core/src/org/sleuthkit/autopsy/timeline/images/eye_close.png
Normal file
After Width: | Height: | Size: 545 B |
BIN
Core/src/org/sleuthkit/autopsy/timeline/images/marker--arrow.png
Normal file
After Width: | Height: | Size: 672 B |
After Width: | Height: | Size: 695 B |
BIN
Core/src/org/sleuthkit/autopsy/timeline/images/marker--minus.png
Normal file
After Width: | Height: | Size: 567 B |
After Width: | Height: | Size: 660 B |
BIN
Core/src/org/sleuthkit/autopsy/timeline/images/marker--pin.png
Normal file
After Width: | Height: | Size: 497 B |
BIN
Core/src/org/sleuthkit/autopsy/timeline/images/marker--plus.png
Normal file
After Width: | Height: | Size: 656 B |
@ -45,8 +45,6 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.effect.Effect;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
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
|
||||
* 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.")
|
||||
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;
|
||||
|
||||
public TimeLineController getController() {
|
||||
return controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the list of nodes containing settings widgets to insert into this
|
||||
* visualization's header
|
||||
@ -195,6 +197,7 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
|
||||
*/
|
||||
abstract protected Axis<Y> getYAxis();
|
||||
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
abstract protected void resetData();
|
||||
|
||||
/**
|
||||
@ -378,14 +381,6 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
|
||||
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
|
||||
* labels
|
||||
@ -512,12 +507,13 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
|
||||
MaskerPane maskerPane = new MaskerPane();
|
||||
maskerPane.textProperty().bind(messageProperty());
|
||||
maskerPane.progressProperty().bind(progressProperty());
|
||||
setCenter(new StackPane(chart, maskerPane));
|
||||
setCenter(new StackPane(center, maskerPane));
|
||||
setCursor(Cursor.WAIT);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
private final Node center = getCenter();
|
||||
|
||||
/**
|
||||
* updates the horisontal axis and removes the blocking progress
|
||||
@ -530,7 +526,7 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
|
||||
layoutDateLabels();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
setCenter(chart); //clear masker pane
|
||||
setCenter(center); //clear masker pane
|
||||
setCursor(Cursor.DEFAULT);
|
||||
});
|
||||
}
|
||||
@ -543,11 +539,11 @@ public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, Cha
|
||||
*/
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.NOT_UI)
|
||||
protected void resetChart(AxisValuesType axisValues) {
|
||||
resetData();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
resetData();
|
||||
setDateAxisValues(axisValues);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
abstract protected void setDateAxisValues(AxisValuesType values);
|
||||
|
@ -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);
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.image.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import java.lang.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?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>
|
||||
<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>
|
||||
@ -22,7 +25,7 @@
|
||||
<Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
|
||||
</padding>
|
||||
<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>
|
||||
<HBox alignment="BOTTOM_LEFT" BorderPane.alignment="BOTTOM_LEFT">
|
||||
<children>
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2014-15 Basis Technology Corp.
|
||||
* Copyright 2014-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* 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.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
@ -34,6 +35,8 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
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.ActionUtils;
|
||||
import org.joda.time.DateTime;
|
||||
@ -53,7 +56,7 @@ import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
*/
|
||||
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 double STROKE_WIDTH = 3;
|
||||
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
|
||||
*/
|
||||
public final TimeLineChart<X> chart;
|
||||
public final IntervalSelectorProvider<X> chart;
|
||||
|
||||
private Tooltip tooltip;
|
||||
/////////drag state
|
||||
@ -89,7 +92,7 @@ public abstract class IntervalSelector<X> extends BorderPane {
|
||||
@FXML
|
||||
private BorderPane bottomBorder;
|
||||
|
||||
public IntervalSelector(TimeLineChart<X> chart) {
|
||||
public IntervalSelector(IntervalSelectorProvider<X> chart) {
|
||||
this.chart = chart;
|
||||
this.controller = chart.getController();
|
||||
FXMLConstructor.construct(this, IntervalSelector.class, "IntervalSelector.fxml"); // NON-NLS
|
||||
@ -107,7 +110,7 @@ public abstract class IntervalSelector<X> extends BorderPane {
|
||||
setMaxWidth(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.managedProperty().bind(showingControls);
|
||||
zoomButton.visibleProperty().bind(showingControls);
|
||||
@ -115,8 +118,9 @@ public abstract class IntervalSelector<X> extends BorderPane {
|
||||
|
||||
widthProperty().addListener(o -> {
|
||||
IntervalSelector.this.updateStartAndEnd();
|
||||
if (startLabel.getWidth() + zoomButton.getWidth() + endLabel.getWidth() > getWidth()) {
|
||||
if (startLabel.getWidth() + zoomButton.getWidth() + endLabel.getWidth() > getWidth() - 10) {
|
||||
this.setCenter(zoomButton);
|
||||
bottomBorder.setCenter(new Rectangle(10, 10, Color.TRANSPARENT));
|
||||
} else {
|
||||
bottomBorder.setCenter(zoomButton);
|
||||
}
|
||||
@ -154,7 +158,11 @@ public abstract class IntervalSelector<X> extends BorderPane {
|
||||
mousePress.consume();
|
||||
});
|
||||
|
||||
setOnMouseReleased(mouseRelease -> isDragging.set(false));
|
||||
setOnMouseReleased((MouseEvent mouseRelease) -> {
|
||||
isDragging.set(false);
|
||||
mouseRelease.consume();;
|
||||
});
|
||||
|
||||
setOnMouseDragged(mouseDrag -> {
|
||||
isDragging.set(true);
|
||||
double dX = mouseDrag.getScreenX() - startDragX;
|
||||
@ -188,20 +196,17 @@ public abstract class IntervalSelector<X> extends BorderPane {
|
||||
mouseDrag.consume();
|
||||
});
|
||||
|
||||
ActionUtils.configureButton(new ZoomToSelectedIntervalAction(), zoomButton);
|
||||
ActionUtils.configureButton(new ClearSelectedIntervalAction(), closeButton);
|
||||
|
||||
//have to add handler rather than use convenience methods so that charts can listen for dismisal click
|
||||
setOnMouseClicked(mosueClick -> {
|
||||
if (mosueClick.getButton() == MouseButton.SECONDARY) {
|
||||
setOnMouseClicked(mouseClick -> {
|
||||
if (mouseClick.getButton() == MouseButton.SECONDARY) {
|
||||
chart.clearIntervalSelector();
|
||||
mosueClick.consume();
|
||||
}
|
||||
if (mosueClick.getClickCount() >= 2) {
|
||||
} else if (mouseClick.getClickCount() >= 2) {
|
||||
zoomToSelectedInterval();
|
||||
mosueClick.consume();
|
||||
mouseClick.consume();
|
||||
}
|
||||
});
|
||||
|
||||
ActionUtils.configureButton(new ZoomToSelectedIntervalAction(), zoomButton);
|
||||
ActionUtils.configureButton(new ClearSelectedIntervalAction(), closeButton);
|
||||
}
|
||||
|
||||
private Point2D getLocalMouseCoords(MouseEvent mouseEvent) {
|
||||
@ -246,7 +251,7 @@ public abstract class IntervalSelector<X> extends BorderPane {
|
||||
|
||||
@NbBundle.Messages(value = {"# {0} - start 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() {
|
||||
String startString = formatSpan(getSpanStart());
|
||||
String endString = formatSpan(getSpanEnd());
|
||||
@ -307,10 +312,35 @@ public abstract class IntervalSelector<X> extends BorderPane {
|
||||
ClearSelectedIntervalAction() {
|
||||
super("");
|
||||
setLongText(Bundle.IntervalSelector_ClearSelectedIntervalAction_tooltTipText());
|
||||
setGraphic(new ImageView(ClEAR_INTERVAL_ICON));
|
||||
setGraphic(new ImageView(CLEAR_INTERVAL_ICON));
|
||||
setEventHandler((ActionEvent t) -> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2014-15 Basis Technology Corp.
|
||||
* Copyright 2014-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -18,28 +18,32 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.ui;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.event.EventType;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.chart.Chart;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.controlsfx.control.action.ActionGroup;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.actions.Back;
|
||||
import org.sleuthkit.autopsy.timeline.actions.Forward;
|
||||
import org.sleuthkit.autopsy.timeline.ui.IntervalSelector.IntervalSelectorProvider;
|
||||
|
||||
/**
|
||||
* Interface for TimeLineViews that are 'charts'.
|
||||
*
|
||||
* @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();
|
||||
|
||||
void setIntervalSelector(IntervalSelector<? extends X> newIntervalSelector);
|
||||
@ -62,10 +66,6 @@ public interface TimeLineChart<X> {
|
||||
|
||||
public TimeLineController getController();
|
||||
|
||||
ContextMenu getChartContextMenu();
|
||||
|
||||
ContextMenu getChartContextMenu(MouseEvent m);
|
||||
|
||||
/**
|
||||
* drag handler class used by {@link TimeLineChart}s to create
|
||||
* {@link IntervalSelector}s
|
||||
@ -73,7 +73,7 @@ public interface TimeLineChart<X> {
|
||||
* @param <X> the type of values along the horizontal axis
|
||||
* @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;
|
||||
|
||||
@ -97,15 +97,13 @@ public interface TimeLineChart<X> {
|
||||
chart.getIntervalSelector().prefHeightProperty().bind(chart.heightProperty());
|
||||
startX = mouseEvent.getX();
|
||||
chart.getIntervalSelector().relocate(startX, 0);
|
||||
} else {
|
||||
} else if (mouseEvent.getX() > startX) {
|
||||
//resize/position existing selector
|
||||
if (mouseEvent.getX() > startX) {
|
||||
chart.getIntervalSelector().relocate(startX, 0);
|
||||
chart.getIntervalSelector().setPrefWidth(mouseEvent.getX() - startX);
|
||||
} else {
|
||||
chart.getIntervalSelector().relocate(mouseEvent.getX(), 0);
|
||||
chart.getIntervalSelector().setPrefWidth(startX - mouseEvent.getX());
|
||||
}
|
||||
chart.getIntervalSelector().relocate(startX, 0);
|
||||
chart.getIntervalSelector().setPrefWidth(mouseEvent.getX() - startX);
|
||||
} else {
|
||||
chart.getIntervalSelector().relocate(mouseEvent.getX(), 0);
|
||||
chart.getIntervalSelector().setPrefWidth(startX - mouseEvent.getX());
|
||||
}
|
||||
chart.getIntervalSelector().autosize();
|
||||
} else if (mouseEventType == MouseEvent.MOUSE_RELEASED) {
|
||||
@ -114,10 +112,9 @@ public interface TimeLineChart<X> {
|
||||
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;
|
||||
|
||||
@ -126,30 +123,18 @@ public interface TimeLineChart<X> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MouseEvent clickEvent) {
|
||||
if (chart.getChartContextMenu() != null) {
|
||||
chart.getChartContextMenu().hide();
|
||||
}
|
||||
if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) {
|
||||
chart.getChartContextMenu(clickEvent);
|
||||
chart.setOnMouseMoved(this);
|
||||
chart.getChartContextMenu().show(chart, clickEvent.getScreenX(), clickEvent.getScreenY());
|
||||
clickEvent.consume();
|
||||
public void handle(MouseEvent mouseEvent) {
|
||||
if (MouseEvent.MOUSE_CLICKED == mouseEvent.getEventType() && mouseEvent.isPopupTrigger() && mouseEvent.isStillSincePress()) {
|
||||
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();
|
||||
}
|
||||
mouseEvent.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"})
|
||||
static ActionGroup newZoomHistoyActionGroup(TimeLineController controller) {
|
||||
return new ActionGroup(Bundle.TimeLineChart_zoomHistoryActionGroup_name(),
|
||||
|
@ -383,7 +383,10 @@ final public class VisualizationPanel extends BorderPane {
|
||||
|
||||
notificationPane.setContent(visualization);
|
||||
if (visualization instanceof DetailViewPane) {
|
||||
eventsTree.setDetailViewPane((DetailViewPane) visualization);
|
||||
Platform.runLater(() -> {
|
||||
((DetailViewPane) visualization).setHighLightedEvents(eventsTree.getSelectedEvents());
|
||||
eventsTree.setDetailViewPane((DetailViewPane) visualization);
|
||||
});
|
||||
}
|
||||
visualization.hasEvents.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue == false) {
|
||||
@ -391,11 +394,11 @@ final public class VisualizationPanel extends BorderPane {
|
||||
notificationPane.setContent(
|
||||
new StackPane(visualization,
|
||||
new Region() {
|
||||
{
|
||||
setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
setOpacity(.3);
|
||||
}
|
||||
},
|
||||
{
|
||||
setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
setOpacity(.3);
|
||||
}
|
||||
},
|
||||
new NoEventsDialog(() -> notificationPane.setContent(visualization))));
|
||||
} else {
|
||||
notificationPane.setContent(visualization);
|
||||
@ -426,8 +429,8 @@ final public class VisualizationPanel extends BorderPane {
|
||||
NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.title"), true) { // NON-NLS
|
||||
private final Lighting lighting = new Lighting();
|
||||
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
|
||||
updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.preparing")); // NON-NLS
|
||||
|
||||
@ -473,28 +476,28 @@ final public class VisualizationPanel extends BorderPane {
|
||||
|
||||
histogramBox.getChildren().clear();
|
||||
|
||||
for (Long bin : fbins) {
|
||||
if (isCancelled()) {
|
||||
break;
|
||||
}
|
||||
Region bar = new Region();
|
||||
//scale them to fit in histogram height
|
||||
bar.prefHeightProperty().bind(histogramBox.heightProperty().multiply(Math.log(bin)).divide(fMax));
|
||||
bar.setMaxHeight(USE_PREF_SIZE);
|
||||
bar.setMinHeight(USE_PREF_SIZE);
|
||||
bar.setBackground(background);
|
||||
bar.setOnMouseEntered((MouseEvent event) -> {
|
||||
Tooltip.install(bar, new Tooltip(bin.toString()));
|
||||
});
|
||||
bar.setEffect(lighting);
|
||||
//they each get equal width to fill the histogram horizontally
|
||||
HBox.setHgrow(bar, Priority.ALWAYS);
|
||||
histogramBox.getChildren().add(bar);
|
||||
}
|
||||
for (Long bin : fbins) {
|
||||
if (isCancelled()) {
|
||||
break;
|
||||
}
|
||||
Region bar = new Region();
|
||||
//scale them to fit in histogram height
|
||||
bar.prefHeightProperty().bind(histogramBox.heightProperty().multiply(Math.log(bin)).divide(fMax));
|
||||
bar.setMaxHeight(USE_PREF_SIZE);
|
||||
bar.setMinHeight(USE_PREF_SIZE);
|
||||
bar.setBackground(background);
|
||||
bar.setOnMouseEntered((MouseEvent event) -> {
|
||||
Tooltip.install(bar, new Tooltip(bin.toString()));
|
||||
});
|
||||
bar.setEffect(lighting);
|
||||
//they each get equal width to fill the histogram horizontally
|
||||
HBox.setHgrow(bar, Priority.ALWAYS);
|
||||
histogramBox.getChildren().add(bar);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
};
|
||||
new Thread(histogramTask).start();
|
||||
|
@ -45,6 +45,7 @@ import javafx.scene.layout.Region;
|
||||
import org.joda.time.Interval;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
|
||||
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
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) {
|
||||
super(controller, partPane, contextPane, spacer);
|
||||
chart = new EventCountsChart(controller, dateAxis, countAxis, selectedNodes);
|
||||
setChartClickHandler();
|
||||
|
||||
chart.setData(dataSeries);
|
||||
setCenter(chart);
|
||||
|
||||
@ -201,18 +202,16 @@ public class CountsViewPane extends AbstractVisualizationPane<String, Number, No
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
@Override
|
||||
protected void resetData() {
|
||||
for (XYChart.Series<String, Number> s : dataSeries) {
|
||||
s.getData().clear();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
for (XYChart.Series<String, Number> s : dataSeries) {
|
||||
s.getData().clear();
|
||||
}
|
||||
|
||||
dataSeries.clear();
|
||||
eventTypeToSeriesMap.clear();
|
||||
createSeries();
|
||||
});
|
||||
dataSeries.clear();
|
||||
eventTypeToSeriesMap.clear();
|
||||
createSeries();
|
||||
}
|
||||
|
||||
private static enum ScaleType implements Function<Long, Double> {
|
||||
|
@ -19,7 +19,6 @@
|
||||
package org.sleuthkit.autopsy.timeline.ui.countsview;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.MissingResourceException;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.collections.ObservableList;
|
||||
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 ContextMenu chartContextMenu;
|
||||
|
||||
@Override
|
||||
public ContextMenu getChartContextMenu() {
|
||||
return chartContextMenu;
|
||||
}
|
||||
|
||||
private final TimeLineController controller;
|
||||
private final FilteredEventsModel filteredEvents;
|
||||
|
||||
@ -114,6 +108,10 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
|
||||
setOnMouseClicked(new MouseClickedHandler<>(this));
|
||||
|
||||
this.selectedNodes = selectedNodes;
|
||||
|
||||
getController().getEventsModel().timeRangeProperty().addListener(o -> {
|
||||
clearIntervalSelector();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -122,8 +120,22 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
|
||||
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
|
||||
public ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException {
|
||||
public ContextMenu getContextMenu(MouseEvent clickEvent) {
|
||||
if (chartContextMenu != null) {
|
||||
chartContextMenu.hide();
|
||||
}
|
||||
@ -155,14 +167,8 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
|
||||
return new CountsIntervalSelector(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* used by {@link CountsViewPane#BarClickHandler} to close the context menu
|
||||
* when the bar menu is requested
|
||||
*
|
||||
* @return the context menu for this chart
|
||||
*/
|
||||
ContextMenu getContextMenu() {
|
||||
return chartContextMenu;
|
||||
public ObservableList<Node> getSelectedNodes() {
|
||||
return selectedNodes;
|
||||
}
|
||||
|
||||
void setRangeInfo(RangeDivisionInfo rangeInfo) {
|
||||
@ -370,7 +376,7 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
|
||||
controller.selectTimeAndType(interval, type);
|
||||
selectedNodes.setAll(node);
|
||||
} else if (e.getButton().equals(MouseButton.SECONDARY)) {
|
||||
getChartContextMenu(e).hide();
|
||||
getContextMenu(e).hide();
|
||||
|
||||
if (barContextMenu == null) {
|
||||
barContextMenu = new ContextMenu();
|
||||
@ -382,7 +388,7 @@ final class EventCountsChart extends StackedBarChart<String, Number> implements
|
||||
new SeparatorMenuItem(),
|
||||
ActionUtils.createMenuItem(new ZoomToIntervalAction()));
|
||||
|
||||
barContextMenu.getItems().addAll(getChartContextMenu(e).getItems());
|
||||
barContextMenu.getItems().addAll(getContextMenu(e).getItems());
|
||||
}
|
||||
|
||||
barContextMenu.show(node, e.getScreenX(), e.getScreenY());
|
||||
|
@ -4,8 +4,8 @@ DetailViewPane.bandByTypeBox.text=Band by Type
|
||||
DetailViewPane.bandByTypeBoxMenuItem.text=Band by Type
|
||||
DetailViewPane.oneEventPerRowBox.text=One Per Row
|
||||
DetailViewPane.oneEventPerRowBoxMenuItem.text=One Per Row
|
||||
DetailViewPan.truncateAllBox.text=Truncate Descriptions
|
||||
DetailViewPan.truncateAllBoxMenuItem.text=Truncate Descriptions
|
||||
DetailViewPane.truncateAllBox.text=Truncate Descriptions
|
||||
DetailViewPane.truncateAllBoxMenuItem.text=Truncate Descriptions
|
||||
DetailViewPane.truncateSlideLabelMenuItem.text=max description width (px)
|
||||
DetailViewPane.descVisSeparatorMenuItem.text=Description Visibility
|
||||
DetailViewPane.showRadioMenuItem.text=Show Full Description
|
||||
@ -13,4 +13,5 @@ DetailViewPane.showRadio.text=Show Full Description
|
||||
DetailViewPane.countsRadioMenuItem.text=Show Counts Only
|
||||
DetailViewPane.countsRadio.text=Show Counts Only
|
||||
DetailViewPane.hiddenRadioMenuItem.text=Hide Description
|
||||
DetailViewPane.hiddenRadio.text=Hide Description
|
||||
DetailViewPane.hiddenRadio.text=Hide Description
|
||||
|
||||
|
@ -20,9 +20,9 @@
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
@ -51,7 +51,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
|
||||
*/
|
||||
final class DateAxis extends Axis<DateTime> {
|
||||
|
||||
private ObjectProperty<DateTime> lowerBound = new ObjectPropertyBase<DateTime>() {
|
||||
private ObjectProperty<DateTime> lowerBound = new ObjectPropertyBase<DateTime>(new DateTime(0)) {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
if (!isAutoRanging()) {
|
||||
@ -89,7 +89,7 @@ final class DateAxis extends Axis<DateTime> {
|
||||
|
||||
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
|
||||
protected void invalidated() {
|
||||
if (!isAutoRanging()) {
|
||||
@ -114,7 +114,11 @@ final class DateAxis extends Axis<DateTime> {
|
||||
* by the data.
|
||||
*/
|
||||
DateAxis() {
|
||||
setTickLabelGap(0);
|
||||
setAutoRanging(false);
|
||||
setTickLabelsVisible(false);
|
||||
setTickLength(0);
|
||||
setTickMarkVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -147,7 +151,7 @@ final class DateAxis extends Axis<DateTime> {
|
||||
*
|
||||
* @see #lowerBoundProperty()
|
||||
*/
|
||||
public final DateTime getLowerBound() {
|
||||
public DateTime getLowerBound() {
|
||||
return lowerBound.get();
|
||||
}
|
||||
|
||||
@ -158,7 +162,7 @@ final class DateAxis extends Axis<DateTime> {
|
||||
*
|
||||
* @see #lowerBoundProperty()
|
||||
*/
|
||||
public final void setLowerBound(DateTime date) {
|
||||
public void setLowerBound(DateTime date) {
|
||||
lowerBound.set(date);
|
||||
}
|
||||
|
||||
@ -169,7 +173,7 @@ final class DateAxis extends Axis<DateTime> {
|
||||
*
|
||||
* @see #upperBoundProperty()
|
||||
*/
|
||||
public final DateTime getUpperBound() {
|
||||
public DateTime getUpperBound() {
|
||||
return upperBound.get();
|
||||
}
|
||||
|
||||
@ -180,7 +184,7 @@ final class DateAxis extends Axis<DateTime> {
|
||||
*
|
||||
* @see #upperBoundProperty() ()
|
||||
*/
|
||||
public final void setUpperBound(DateTime date) {
|
||||
public void setUpperBound(DateTime date) {
|
||||
upperBound.set(date);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2014-15 Basis Technology Corp.
|
||||
* Copyright 2014-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* 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.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.chart.XYChart;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
@ -38,194 +37,127 @@ import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.CustomMenuItem;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuButton;
|
||||
import javafx.scene.control.MultipleSelectionModel;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.ScrollBar;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.control.TreeItem;
|
||||
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.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Modality;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.controlsfx.control.action.Action;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Interval;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
|
||||
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
|
||||
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.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Controller class for a {@link EventDetailsChart} based implementation of a
|
||||
* Controller class for a {@link PrimaryDetailsChart} based implementation of a
|
||||
* TimeLineView.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Concurrency Policy: Access to the private members clusterChart, dateAxis,
|
||||
* EventTypeMap, and dataSets is all linked directly to the ClusterChart which
|
||||
* 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 static final double LINE_SCROLL_PERCENTAGE = .10;
|
||||
private static final double PAGE_SCROLL_PERCENTAGE = .70;
|
||||
private final DateAxis detailsChartDateAxis = new DateAxis();
|
||||
private final DateAxis pinnedDateAxis = new DateAxis();
|
||||
|
||||
private final DateAxis dateAxis = new DateAxis();
|
||||
private final Axis<EventStripe> verticalAxis = new EventAxis();
|
||||
private final ScrollBar vertScrollBar = new ScrollBar();
|
||||
private final Region scrollBarSpacer = new Region();
|
||||
@NbBundle.Messages("DetailViewPane.primaryLaneLabel.text=All Events (Filtered)")
|
||||
private final Axis<EventStripe> verticalAxis = new EventAxis<>(Bundle.DetailViewPane_primaryLaneLabel_text());
|
||||
|
||||
private MultipleSelectionModel<TreeItem<EventBundle<?>>> treeSelectionModel;
|
||||
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);
|
||||
});
|
||||
|
||||
}
|
||||
private final MappedList<TimeLineEvent, EventNodeBase<?>> selectedEvents;
|
||||
|
||||
public DetailViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region bottomLeftSpacer) {
|
||||
super(controller, partPane, contextPane, bottomLeftSpacer);
|
||||
this.selectedEvents = new MappedList<>(getSelectedNodes(), EventNodeBase<?>::getEvent);
|
||||
|
||||
//initialize chart;
|
||||
chart = new EventDetailsChart(controller, dateAxis, verticalAxis, selectedNodes);
|
||||
setChartClickHandler(); //can we push this into chart
|
||||
chart.setData(dataSeries);
|
||||
chart = new DetailsChart(controller, detailsChartDateAxis, pinnedDateAxis, verticalAxis, getSelectedNodes());
|
||||
setCenter(chart);
|
||||
settingsNodes = new ArrayList<>(new DetailViewSettingsPane(chart.getLayoutSettings()).getChildrenUnmodifiable());
|
||||
|
||||
settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable());
|
||||
//bind layout fo axes and spacers
|
||||
dateAxis.setTickLabelGap(0);
|
||||
dateAxis.setAutoRanging(false);
|
||||
dateAxis.setTickLabelsVisible(false);
|
||||
dateAxis.getTickMarks().addListener((Observable observable) -> layoutDateLabels());
|
||||
dateAxis.getTickSpacing().addListener(observable -> layoutDateLabels());
|
||||
|
||||
// //bind layout fo axes and spacers
|
||||
detailsChartDateAxis.getTickMarks().addListener((Observable observable) -> layoutDateLabels());
|
||||
detailsChartDateAxis.getTickSpacing().addListener(observable -> layoutDateLabels());
|
||||
verticalAxis.setAutoRanging(false); //prevent XYChart.updateAxisRange() from accessing dataSeries on JFX thread causing ConcurrentModificationException
|
||||
bottomLeftSpacer.minWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty()));
|
||||
bottomLeftSpacer.prefWidthProperty().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) -> {
|
||||
highlightedNodes.clear();
|
||||
selectedNodes.stream().forEach((tn) -> {
|
||||
for (EventBundleNodeBase<?, ?, ?> n : chart.getNodes((EventBundleNodeBase<?, ?, ?> t) ->
|
||||
t.getDescription().equals(tn.getDescription()))) {
|
||||
highlightedNodes.add(n);
|
||||
}
|
||||
});
|
||||
chart.setHighlightPredicate(selectedNodes::contains);
|
||||
getController().selectEventIDs(selectedNodes.stream()
|
||||
.flatMap(detailNode -> detailNode.getEventIDs().stream())
|
||||
.collect(Collectors.toList()));
|
||||
});
|
||||
}
|
||||
|
||||
private void incrementScrollValue(double factor) {
|
||||
vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() + factor * chart.getHeight()));
|
||||
public ObservableList<EventStripe> getAllEventStripes() {
|
||||
return chart.getAllNestedEventStripes();
|
||||
}
|
||||
|
||||
private Double clampScroll(Double value) {
|
||||
return Math.max(0, Math.min(vertScrollBar.getMax() + 50, value));
|
||||
public ObservableList<TimeLineEvent> getSelectedEvents() {
|
||||
return selectedEvents;
|
||||
}
|
||||
|
||||
public void setSelectionModel(MultipleSelectionModel<TreeItem<EventBundle<?>>> selectionModel) {
|
||||
this.treeSelectionModel = selectionModel;
|
||||
|
||||
treeSelectionModel.getSelectedItems().addListener((Observable observable) -> {
|
||||
highlightedNodes.clear();
|
||||
for (TreeItem<EventBundle<?>> tn : treeSelectionModel.getSelectedItems()) {
|
||||
|
||||
for (EventBundleNodeBase<?, ?, ?> n : chart.getNodes((EventBundleNodeBase<?, ?, ?> t) ->
|
||||
t.getDescription().equals(tn.getValue().getDescription()))) {
|
||||
highlightedNodes.add(n);
|
||||
}
|
||||
}
|
||||
public void setHighLightedEvents(ObservableList<TimeLineEvent> highlightedEvents) {
|
||||
highlightedEvents.addListener((Observable observable) -> {
|
||||
Predicate<EventNodeBase<?>> highlightPredicate =
|
||||
highlightedEvents.stream()
|
||||
.map(TimeLineEvent::getDescription)
|
||||
.map(new Function<String, Predicate<EventNodeBase<?>>>() {
|
||||
@Override
|
||||
public Predicate<EventNodeBase<?>> apply(String description) {
|
||||
return eventNode -> StringUtils.equalsIgnoreCase(eventNode.getDescription(), description);
|
||||
}
|
||||
})
|
||||
.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
|
||||
protected Boolean isTickBold(DateTime value) {
|
||||
return false;
|
||||
@ -236,19 +168,14 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
|
||||
return verticalAxis;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Axis<DateTime> getXAxis() {
|
||||
return dateAxis;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double getTickSpacing() {
|
||||
return dateAxis.getTickSpacing().get();
|
||||
return detailsChartDateAxis.getTickSpacing().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTickMarkLabel(DateTime value) {
|
||||
return dateAxis.getTickMarkLabel(value);
|
||||
return detailsChartDateAxis.getTickMarkLabel(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -262,11 +189,17 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applySelectionEffect(EventBundleNodeBase<?, ?, ?> c1, Boolean selected) {
|
||||
protected void applySelectionEffect(EventNodeBase<?> c1, Boolean selected) {
|
||||
c1.applySelectionEffect(selected);
|
||||
|
||||
}
|
||||
|
||||
private class DetailViewSettingsPane extends HBox {
|
||||
DateTime getDateTimeForPosition(double layoutX) {
|
||||
return chart.getDateTimeForPosition(layoutX);
|
||||
|
||||
}
|
||||
|
||||
static private class DetailViewSettingsPane extends HBox {
|
||||
|
||||
@FXML
|
||||
private RadioButton hiddenRadio;
|
||||
@ -322,7 +255,12 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
|
||||
@FXML
|
||||
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
|
||||
}
|
||||
|
||||
@ -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 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
|
||||
bandByTypeBox.selectedProperty().bindBidirectional(chart.bandByTypeProperty());
|
||||
truncateAllBox.selectedProperty().bindBidirectional(chart.truncateAllProperty());
|
||||
oneEventPerRowBox.selectedProperty().bindBidirectional(chart.oneEventPerRowProperty());
|
||||
assert pinnedEventsToggle != null : "fx:id=\"pinnedEventsToggle\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
|
||||
bandByTypeBox.selectedProperty().bindBidirectional(layoutSettings.bandByTypeProperty());
|
||||
truncateAllBox.selectedProperty().bindBidirectional(layoutSettings.truncateAllProperty());
|
||||
oneEventPerRowBox.selectedProperty().bindBidirectional(layoutSettings.oneEventPerRowProperty());
|
||||
truncateSliderLabel.disableProperty().bind(truncateAllBox.selectedProperty().not());
|
||||
truncateSliderLabel.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateSliderLabel.text"));
|
||||
pinnedEventsToggle.selectedProperty().bindBidirectional(layoutSettings.pinnedLaneShowing());
|
||||
|
||||
final InvalidationListener sliderListener = o -> {
|
||||
if (truncateWidthSlider.isValueChanging() == false) {
|
||||
chart.getTruncateWidth().set(truncateWidthSlider.getValue());
|
||||
layoutSettings.truncateWidthProperty().set(truncateWidthSlider.getValue());
|
||||
}
|
||||
};
|
||||
truncateWidthSlider.valueProperty().addListener(sliderListener);
|
||||
@ -347,29 +287,25 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
|
||||
|
||||
descrVisibility.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> {
|
||||
if (newToggle == countsRadio) {
|
||||
chart.descrVisibilityProperty().set(DescriptionVisibility.COUNT_ONLY);
|
||||
layoutSettings.descrVisibilityProperty().set(DescriptionVisibility.COUNT_ONLY);
|
||||
} else if (newToggle == showRadio) {
|
||||
chart.descrVisibilityProperty().set(DescriptionVisibility.SHOWN);
|
||||
layoutSettings.descrVisibilityProperty().set(DescriptionVisibility.SHOWN);
|
||||
} else if (newToggle == hiddenRadio) {
|
||||
chart.descrVisibilityProperty().set(DescriptionVisibility.HIDDEN);
|
||||
layoutSettings.descrVisibilityProperty().set(DescriptionVisibility.HIDDEN);
|
||||
}
|
||||
});
|
||||
|
||||
advancedLayoutOptionsButtonLabel.setText(
|
||||
NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.advancedLayoutOptionsButtonLabel.text"));
|
||||
truncateSliderLabel.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateSliderLabel.text"));
|
||||
|
||||
advancedLayoutOptionsButtonLabel.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.advancedLayoutOptionsButtonLabel.text"));
|
||||
bandByTypeBox.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.bandByTypeBox.text"));
|
||||
bandByTypeBoxMenuItem.setText(
|
||||
NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.bandByTypeBoxMenuItem.text"));
|
||||
bandByTypeBoxMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.bandByTypeBoxMenuItem.text"));
|
||||
oneEventPerRowBox.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.oneEventPerRowBox.text"));
|
||||
oneEventPerRowBoxMenuItem.setText(
|
||||
NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.oneEventPerRowBoxMenuItem.text"));
|
||||
truncateAllBox.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPan.truncateAllBox.text"));
|
||||
truncateAllBoxMenuItem.setText(
|
||||
NbBundle.getMessage(DetailViewPane.class, "DetailViewPan.truncateAllBoxMenuItem.text"));
|
||||
truncateSliderLabelMenuItem.setText(
|
||||
NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateSlideLabelMenuItem.text"));
|
||||
descVisibilitySeparatorMenuItem.setText(
|
||||
NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.descVisSeparatorMenuItem.text"));
|
||||
oneEventPerRowBoxMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.oneEventPerRowBoxMenuItem.text"));
|
||||
truncateAllBox.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateAllBox.text"));
|
||||
truncateAllBoxMenuItem.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.truncateAllBoxMenuItem.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"));
|
||||
showRadio.setText(NbBundle.getMessage(DetailViewPane.class, "DetailViewPane.showRadio.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({
|
||||
"DetailViewPane.loggedTask.queryDb=Retreiving event data",
|
||||
"DetailViewPane.loggedTask.name=Updating Details View",
|
||||
@ -441,12 +369,9 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
|
||||
return null;
|
||||
}
|
||||
updateProgress(i, size);
|
||||
final EventStripe cluster = eventStripes.get(i);
|
||||
final XYChart.Data<DateTime, EventStripe> dataItem = new XYChart.Data<>(new DateTime(cluster.getStartMillis()), cluster);
|
||||
getSeries(cluster.getEventType()).getData().add(dataItem);
|
||||
chart.addDataItem(dataItem);
|
||||
final EventStripe stripe = eventStripes.get(i);
|
||||
Platform.runLater(() -> chart.addStripe(stripe));
|
||||
}
|
||||
|
||||
return eventStripes.isEmpty() == false;
|
||||
}
|
||||
|
||||
@ -458,7 +383,9 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
|
||||
|
||||
@Override
|
||||
protected void setDateAxisValues(Interval timeRange) {
|
||||
dateAxis.setRange(timeRange, true);
|
||||
detailsChartDateAxis.setRange(timeRange, true);
|
||||
pinnedDateAxis.setRange(timeRange, true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<?import javafx.scene.control.CustomMenuItem?>
|
||||
<?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>
|
||||
<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">
|
||||
<items>
|
||||
<CustomMenuItem fx:id="bandByTypeBoxMenuItem" hideOnClick="false" mnemonicParsing="false">
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2014 Basis Technology Corp.
|
||||
* Copyright 2014-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -22,21 +22,25 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javafx.scene.chart.Axis;
|
||||
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
|
||||
* 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
|
||||
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.
|
||||
}
|
||||
|
||||
@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.
|
||||
}
|
||||
|
||||
@ -46,17 +50,17 @@ class EventAxis extends Axis<EventStripe> {
|
||||
}
|
||||
|
||||
@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.
|
||||
}
|
||||
|
||||
@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.
|
||||
}
|
||||
|
||||
@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.
|
||||
}
|
||||
|
||||
@ -66,7 +70,7 @@ class EventAxis extends Axis<EventStripe> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<EventStripe> calculateTickValues(double length, Object range) {
|
||||
protected List<Type> calculateTickValues(double length, Object range) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@ -76,7 +80,7 @@ class EventAxis extends Axis<EventStripe> {
|
||||
}
|
||||
|
||||
@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.
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2013-15 Basis Technology Corp.
|
||||
* Copyright 2013-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -18,16 +18,15 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import static java.util.Objects.nonNull;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Pos;
|
||||
@ -51,11 +50,11 @@ import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
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.datamodel.TimeLineEvent;
|
||||
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
|
||||
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.EventBundleNodeBase.show;
|
||||
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configureActionButton;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
|
||||
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 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 Button plusButton;
|
||||
private Button minusButton;
|
||||
Button plusButton;
|
||||
Button minusButton;
|
||||
|
||||
@Override
|
||||
void installActionButtons() {
|
||||
super.installActionButtons();
|
||||
if (plusButton == null) {
|
||||
plusButton = ActionUtils.createButton(new ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE);
|
||||
minusButton = ActionUtils.createButton(new CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE);
|
||||
plusButton = ActionUtils.createButton(new ExpandClusterAction(this), ActionUtils.ActionTextBehavior.HIDE);
|
||||
minusButton = ActionUtils.createButton(new CollapseClusterAction(this), ActionUtils.ActionTextBehavior.HIDE);
|
||||
controlsHBox.getChildren().addAll(minusButton, plusButton);
|
||||
|
||||
configureLoDButton(plusButton);
|
||||
configureLoDButton(minusButton);
|
||||
infoHBox.getChildren().addAll(minusButton, plusButton);
|
||||
configureActionButton(plusButton);
|
||||
configureActionButton(minusButton);
|
||||
}
|
||||
}
|
||||
|
||||
public EventClusterNode(EventDetailsChart chart, EventCluster eventCluster, EventStripeNode parentNode) {
|
||||
super(chart, eventCluster, parentNode);
|
||||
EventClusterNode(DetailsChartLane<?> chartLane, EventCluster eventCluster, EventStripeNode parentNode) {
|
||||
super(chartLane, eventCluster, parentNode);
|
||||
|
||||
subNodePane.setBorder(clusterBorder);
|
||||
subNodePane.setBackground(defaultBackground);
|
||||
@ -98,45 +97,19 @@ final public class EventClusterNode extends EventBundleNodeBase<EventCluster, Ev
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
setCursor(Cursor.HAND);
|
||||
|
||||
getChildren().addAll(subNodePane, infoHBox);
|
||||
|
||||
if (parentNode == null) {
|
||||
setDescriptionVisibility(DescriptionVisibility.SHOWN);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void showHoverControls(final boolean showControls) {
|
||||
super.showHoverControls(showControls);
|
||||
installActionButtons();
|
||||
show(plusButton, showControls);
|
||||
show(minusButton, showControls);
|
||||
}
|
||||
|
||||
@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;
|
||||
void showFullDescription(final int size) {
|
||||
if (getParentNode().isPresent()) {
|
||||
showCountOnly(size);
|
||||
} else {
|
||||
super.showFullDescription(size);
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,11 +119,10 @@ final public class EventClusterNode extends EventBundleNodeBase<EventCluster, Ev
|
||||
* @param requestedDescrLoD
|
||||
* @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)
|
||||
private synchronized void loadSubBundles(DescriptionLoD.RelativeDetail relativeDetail) {
|
||||
chart.setCursor(Cursor.WAIT);
|
||||
|
||||
private synchronized void loadSubStripes(DescriptionLoD.RelativeDetail relativeDetail) {
|
||||
getChartLane().setCursor(Cursor.WAIT);
|
||||
|
||||
/*
|
||||
* 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 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);
|
||||
|
||||
@Override
|
||||
protected List<EventStripe> call() throws Exception {
|
||||
List<EventStripe> bundles;
|
||||
List<EventStripe> stripes;
|
||||
DescriptionLoD next = loadedDescriptionLoD;
|
||||
do {
|
||||
loadedDescriptionLoD = next;
|
||||
if (loadedDescriptionLoD == getEventBundle().getDescriptionLoD()) {
|
||||
if (loadedDescriptionLoD == getEvent().getDescriptionLoD()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
bundles = eventsModel.getEventStripes(zoomParams.withDescrLOD(loadedDescriptionLoD));
|
||||
stripes = eventsModel.getEventStripes(zoomParams.withDescrLOD(loadedDescriptionLoD));
|
||||
|
||||
next = loadedDescriptionLoD.withRelativeDetail(relativeDetail);
|
||||
} while (bundles.size() == 1 && nonNull(next));
|
||||
} while (stripes.size() == 1 && nonNull(next));
|
||||
|
||||
// return list of EventStripes representing sub-bundles
|
||||
return bundles.stream()
|
||||
.map(eventStripe -> eventStripe.withParent(getEventCluster()))
|
||||
return stripes.stream()
|
||||
.map(eventStripe -> eventStripe.withParent(getEvent()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
try {
|
||||
List<EventStripe> bundles = get();
|
||||
List<EventStripe> newSubStripes = get();
|
||||
|
||||
//clear the existing subnodes
|
||||
List<EventStripe> transform = subNodes.stream().flatMap(new StripeFlattener()).collect(Collectors.toList());
|
||||
chart.getEventStripes().removeAll(transform);
|
||||
List<TimeLineEvent> oldSubStripes = subNodes.stream().flatMap(new StripeFlattener()).collect(Collectors.toList());
|
||||
getChartLane().getParentChart().getAllNestedEventStripes().removeAll(oldSubStripes);
|
||||
subNodes.clear();
|
||||
if (bundles.isEmpty()) {
|
||||
if (newSubStripes.isEmpty()) {
|
||||
getChildren().setAll(subNodePane, infoHBox);
|
||||
descLOD.set(getEventBundle().getDescriptionLoD());
|
||||
setDescriptionLOD(getEvent().getDescriptionLoD());
|
||||
} else {
|
||||
chart.getEventStripes().addAll(bundles);
|
||||
subNodes.addAll(Lists.transform(bundles, EventClusterNode.this::createChildNode));
|
||||
getChartLane().getParentChart().getAllNestedEventStripes().addAll(newSubStripes);
|
||||
subNodes.addAll(Lists.transform(newSubStripes, EventClusterNode.this::createChildNode));
|
||||
getChildren().setAll(new VBox(infoHBox, subNodePane));
|
||||
descLOD.set(loadedDescriptionLoD);
|
||||
setDescriptionLOD(loadedDescriptionLoD);
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); //NON-NLS
|
||||
}
|
||||
chart.requestChartLayout();
|
||||
chart.setCursor(null);
|
||||
getChartLane().requestChartLayout();
|
||||
getChartLane().setCursor(null);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
new Thread(loggedTask).start();
|
||||
//start task
|
||||
chart.getController().monitorTask(loggedTask);
|
||||
getChartLane().getController().monitorTask(loggedTask);
|
||||
}
|
||||
|
||||
@Override
|
||||
EventStripeNode createChildNode(EventStripe stripe) {
|
||||
return new EventStripeNode(chart, stripe, this);
|
||||
}
|
||||
|
||||
EventCluster getEventCluster() {
|
||||
return getEventBundle();
|
||||
EventNodeBase<?> createChildNode(EventStripe stripe) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
double chartX = chart.getXAxis().getDisplayPosition(new DateTime(getStartMillis()));
|
||||
double w = chart.getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX;
|
||||
double chartX = getChartLane().getXAxis().getDisplayPosition(new DateTime(getStartMillis()));
|
||||
double w = getChartLane().getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX;
|
||||
subNodePane.setPrefWidth(Math.max(1, w));
|
||||
super.layoutChildren();
|
||||
}
|
||||
@ -244,52 +217,58 @@ final public class EventClusterNode extends EventBundleNodeBase<EventCluster, Ev
|
||||
RootFilter getSubClusterFilter() {
|
||||
RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf();
|
||||
subClusterFilter.getSubFilters().addAll(
|
||||
new DescriptionFilter(getEventBundle().getDescriptionLoD(), getDescription(), DescriptionFilter.FilterMode.INCLUDE),
|
||||
new DescriptionFilter(getEvent().getDescriptionLoD(), getDescription(), DescriptionFilter.FilterMode.INCLUDE),
|
||||
new TypeFilter(getEventType()));
|
||||
return subClusterFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
Collection<? extends Action> getActions() {
|
||||
return Arrays.asList(new ExpandClusterAction(),
|
||||
new CollapseClusterAction());
|
||||
Iterable<? extends Action> getActions() {
|
||||
return Iterables.concat(
|
||||
super.getActions(),
|
||||
Arrays.asList(new ExpandClusterAction(this), new CollapseClusterAction(this))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
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")
|
||||
ExpandClusterAction() {
|
||||
private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N
|
||||
|
||||
@NbBundle.Messages({"ExpandClusterAction.text=Expand"})
|
||||
ExpandClusterAction(EventClusterNode node) {
|
||||
super(Bundle.ExpandClusterAction_text());
|
||||
|
||||
setGraphic(new ImageView(PLUS));
|
||||
setEventHandler(actionEvent -> {
|
||||
if (descLOD.get().moreDetailed() != null) {
|
||||
loadSubBundles(DescriptionLoD.RelativeDetail.MORE);
|
||||
if (node.getDescriptionLoD().moreDetailed() != null) {
|
||||
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")
|
||||
CollapseClusterAction() {
|
||||
private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N
|
||||
|
||||
@NbBundle.Messages({"CollapseClusterAction.text=Collapse"})
|
||||
CollapseClusterAction(EventClusterNode node) {
|
||||
super(Bundle.CollapseClusterAction_text());
|
||||
|
||||
setGraphic(new ImageView(MINUS));
|
||||
setEventHandler(actionEvent -> {
|
||||
if (descLOD.get().lessDetailed() != null) {
|
||||
loadSubBundles(DescriptionLoD.RelativeDetail.LESS);
|
||||
if (node.getDescriptionLoD().lessDetailed() != null) {
|
||||
node.loadSubStripes(DescriptionLoD.RelativeDetail.LESS);
|
||||
}
|
||||
});
|
||||
disabledProperty().bind(Bindings.createBooleanBinding(() -> nonNull(getEventCluster()) && descLOD.get() == getEventCluster().getDescriptionLoD(), descLOD));
|
||||
|
||||
disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(node.getEvent().getDescriptionLoD()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
/*
|
||||
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2015 Basis Technology Corp.
|
||||
* Copyright 2015-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -19,131 +18,89 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.controlsfx.control.action.Action;
|
||||
import org.controlsfx.control.action.ActionUtils;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
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.
|
||||
*/
|
||||
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 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
|
||||
void installActionButtons() {
|
||||
if (hideButton == null) {
|
||||
hideButton = ActionUtils.createButton(chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD()),
|
||||
ActionUtils.ActionTextBehavior.HIDE);
|
||||
configureLoDButton(hideButton);
|
||||
|
||||
infoHBox.getChildren().add(hideButton);
|
||||
}
|
||||
private Action newHideAction() {
|
||||
return new HideDescriptionAction(getDescription(), getEvent().getDescriptionLoD(), chartLane.getParentChart());
|
||||
}
|
||||
private Button hideButton;
|
||||
|
||||
public EventStripeNode(EventDetailsChart chart, EventStripe eventStripe, EventClusterNode parentNode) {
|
||||
super(chart, eventStripe, parentNode);
|
||||
|
||||
setMinHeight(48);
|
||||
EventStripeNode(DetailsChartLane<?> chartLane, EventStripe eventStripe, EventClusterNode parentNode) {
|
||||
super(chartLane, eventStripe, parentNode);
|
||||
setMinHeight(24);
|
||||
//setup description label
|
||||
eventTypeImageView.setImage(getEventType().getFXImage());
|
||||
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||
descrLabel.setGraphic(eventTypeImageView);
|
||||
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
|
||||
setAlignment(subNodePane, Pos.BOTTOM_LEFT);
|
||||
|
||||
for (EventCluster cluster : eventStripe.getClusters()) {
|
||||
subNodes.add(createChildNode(cluster));
|
||||
if (eventStripe.getClusters().size() > 1) {
|
||||
for (EventCluster cluster : eventStripe.getClusters()) {
|
||||
subNodes.add(createChildNode(cluster.withParent(eventStripe)));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
childNode.setDescriptionVisibility(DescriptionVisibility.HIDDEN);
|
||||
subNodes.add(childNode);
|
||||
getChildren().addAll(infoHBox, subNodePane);
|
||||
}
|
||||
|
||||
getChildren().addAll(new VBox(infoHBox, subNodePane));
|
||||
}
|
||||
|
||||
@Override
|
||||
EventClusterNode createChildNode(EventCluster cluster) {
|
||||
return new EventClusterNode(chart, cluster, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
void showHoverControls(final boolean showControls) {
|
||||
super.showHoverControls(showControls);
|
||||
installActionButtons();
|
||||
show(hideButton, showControls);
|
||||
}
|
||||
|
||||
public EventStripe getEventStripe() {
|
||||
return getEventBundle();
|
||||
return getEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param w the maximum width the description label should have
|
||||
*/
|
||||
@Override
|
||||
public void setMaxDescriptionWidth(double w) {
|
||||
descrLabel.setMaxWidth(w);
|
||||
}
|
||||
void installActionButtons() {
|
||||
super.installActionButtons();
|
||||
if (chartLane.quickHideFiltersEnabled() && hideButton == null) {
|
||||
hideButton = ActionUtils.createButton(newHideAction(),
|
||||
ActionUtils.ActionTextBehavior.HIDE);
|
||||
configureActionButton(hideButton);
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate highlighted nodes
|
||||
*
|
||||
* @param applied true to apply the highlight 'effect', false to remove it
|
||||
*/
|
||||
@Override
|
||||
public 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);
|
||||
controlsHBox.getChildren().add(hideButton);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
EventNodeBase<?> createChildNode(EventCluster cluster) {
|
||||
if (cluster.getEventIDs().size() == 1) {
|
||||
return new SingleEventNode(getChartLane(), getChartLane().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this);
|
||||
} else {
|
||||
return new EventClusterNode(getChartLane(), cluster, this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,7 +111,11 @@ final public class EventStripeNode extends EventBundleNodeBase<EventStripe, Even
|
||||
}
|
||||
|
||||
@Override
|
||||
Collection<? extends Action> getActions() {
|
||||
return Arrays.asList(chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD()));
|
||||
Iterable<? extends Action> getActions() {
|
||||
return Iterables.concat(
|
||||
super.getActions(),
|
||||
Arrays.asList(newHideAction())
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2014-15 Basis Technology Corp.
|
||||
* Copyright 2014-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* 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 final Tooltip tooltip = new Tooltip();
|
||||
private final EventDetailsChart chart;
|
||||
private final DetailsChart chart;
|
||||
|
||||
//used across invocations of mouse event handlers to maintain state
|
||||
private double startLayoutX;
|
||||
@ -47,7 +47,7 @@ class GuideLine extends Line {
|
||||
/**
|
||||
* @param chart the chart this GuideLine belongs to.
|
||||
*/
|
||||
GuideLine(EventDetailsChart chart) {
|
||||
GuideLine(DetailsChart chart) {
|
||||
super(0, 0, 0, 0);
|
||||
this.chart = chart;
|
||||
Axis<DateTime> xAxis = chart.getXAxis();
|
||||
@ -64,8 +64,8 @@ class GuideLine extends Line {
|
||||
|
||||
setOnMouseClicked(clickedEvent -> {
|
||||
if (clickedEvent.getButton() == MouseButton.SECONDARY
|
||||
&& clickedEvent.isStillSincePress() == false) {
|
||||
chart.clearGuideLine();
|
||||
&& clickedEvent.isStillSincePress()) {
|
||||
chart.clearGuideLine(this);
|
||||
clickedEvent.consume();
|
||||
}
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2015 Basis Technology Corp.
|
||||
* Copyright 2015-16 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -20,7 +20,7 @@ package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
import java.util.function.Function;
|
||||
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.
|
||||
@ -28,12 +28,12 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
|
||||
* EventStripes containing the stripes for the given node and all child
|
||||
* eventStripes, ignoring intervening EventCluster nodes.
|
||||
*/
|
||||
class StripeFlattener implements Function<EventStripeNode, Stream<EventStripe>> {
|
||||
class StripeFlattener implements Function<EventNodeBase<?>, Stream<TimeLineEvent>> {
|
||||
|
||||
@Override
|
||||
public Stream<EventStripe> apply(EventStripeNode node) {
|
||||
public Stream<TimeLineEvent> apply(EventNodeBase<?> node) {
|
||||
return Stream.concat(
|
||||
Stream.of(node.getEventStripe()),
|
||||
Stream.of(node.getEvent()),
|
||||
node.getSubNodes().stream().flatMap(clusterNode ->
|
||||
clusterNode.getSubNodes().stream().flatMap(this)));
|
||||
}
|
||||
|
@ -24,56 +24,50 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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
|
||||
*/
|
||||
private final Map<String, EventDescriptionTreeItem> childMap = new HashMap<>();
|
||||
private final EventBundle<?> bundle;
|
||||
private Comparator<TreeItem<EventBundle<?>>> comparator = TreeComparator.Description;
|
||||
private Comparator<TreeItem<TimeLineEvent>> comparator = TreeComparator.Description;
|
||||
|
||||
public EventBundle<?> getEventBundle() {
|
||||
return bundle;
|
||||
}
|
||||
|
||||
EventDescriptionTreeItem(EventBundle<?> g, Comparator<TreeItem<EventBundle<?>>> comp) {
|
||||
bundle = g;
|
||||
EventDescriptionTreeItem(EventStripe stripe, Comparator<TreeItem<TimeLineEvent>> comp) {
|
||||
comparator = comp;
|
||||
setValue(g);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCount() {
|
||||
return getValue().getCount();
|
||||
setValue(stripe);
|
||||
}
|
||||
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
public void insert(Deque<EventBundle<?>> path) {
|
||||
EventBundle<?> head = path.removeFirst();
|
||||
EventDescriptionTreeItem treeItem = childMap.computeIfAbsent(head.getDescription(), description -> {
|
||||
EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(head, comparator);
|
||||
newTreeItem.setExpanded(true);
|
||||
childMap.put(description, newTreeItem);
|
||||
getChildren().add(newTreeItem);
|
||||
resort(comparator, false);
|
||||
return newTreeItem;
|
||||
});
|
||||
public void insert(Deque<EventStripe> path) {
|
||||
EventStripe head = path.removeFirst();
|
||||
String substringAfter = StringUtils.substringAfter(head.getDescription(), head.getParentStripe().map(EventStripe::getDescription).orElse(""));
|
||||
EventDescriptionTreeItem treeItem = childMap.computeIfAbsent(substringAfter,
|
||||
description -> {
|
||||
EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(head, comparator);
|
||||
newTreeItem.setExpanded(true);
|
||||
getChildren().add(newTreeItem);
|
||||
resort(comparator, false);
|
||||
return newTreeItem;
|
||||
});
|
||||
|
||||
if (path.isEmpty() == false) {
|
||||
treeItem.insert(path);
|
||||
}
|
||||
}
|
||||
|
||||
void remove(Deque<EventBundle<?>> path) {
|
||||
EventBundle<?> head = path.removeFirst();
|
||||
EventDescriptionTreeItem descTreeItem = childMap.get(head.getDescription());
|
||||
void remove(Deque<EventStripe> path) {
|
||||
EventStripe head = path.removeFirst();
|
||||
String substringAfter = StringUtils.substringAfter(head.getDescription(), head.getParentStripe().map(EventStripe::getDescription).orElse(""));
|
||||
EventDescriptionTreeItem descTreeItem = childMap.get(substringAfter);
|
||||
if (path.isEmpty() == false) {
|
||||
descTreeItem.remove(path);
|
||||
}
|
||||
@ -84,7 +78,7 @@ class EventDescriptionTreeItem extends NavTreeItem {
|
||||
}
|
||||
|
||||
@Override
|
||||
void resort(Comparator<TreeItem<EventBundle<?>>> comp, Boolean recursive) {
|
||||
void resort(Comparator<TreeItem<TimeLineEvent>> comp, Boolean recursive) {
|
||||
this.comparator = comp;
|
||||
FXCollections.sort(getChildren(), comp);
|
||||
if (recursive) {
|
||||
@ -93,14 +87,14 @@ class EventDescriptionTreeItem extends NavTreeItem {
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) {
|
||||
public EventsTreeItem findTreeItemForEvent(TimeLineEvent event) {
|
||||
|
||||
if (getValue().getEventType() == t.getEventType()
|
||||
&& getValue().getDescription().equals(t.getDescription())) {
|
||||
if (getValue().getEventType() == event.getEventType()
|
||||
&& getValue().getDescription().equals(event.getDescription())) {
|
||||
return this;
|
||||
} else {
|
||||
for (EventDescriptionTreeItem child : childMap.values()) {
|
||||
final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t);
|
||||
final EventsTreeItem findTreeItemForEvent = child.findTreeItemForEvent(event);
|
||||
if (findTreeItemForEvent != null) {
|
||||
return findTreeItemForEvent;
|
||||
}
|
||||
@ -109,4 +103,20 @@ class EventDescriptionTreeItem extends NavTreeItem {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,46 +25,46 @@ import java.util.Map;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.TreeItem;
|
||||
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
|
||||
*/
|
||||
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) {
|
||||
setValue(g);
|
||||
EventTypeTreeItem(EventStripe stripe, Comparator<TreeItem<TimeLineEvent>> comp) {
|
||||
setValue(null);
|
||||
eventType = stripe.getEventType();
|
||||
comparator = comp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCount() {
|
||||
return getValue().getCount();
|
||||
}
|
||||
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
public void insert(Deque<EventBundle<?>> path) {
|
||||
EventBundle<?> head = path.removeFirst();
|
||||
EventDescriptionTreeItem treeItem = childMap.computeIfAbsent(head.getDescription(), description -> {
|
||||
EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(head, comparator);
|
||||
newTreeItem.setExpanded(true);
|
||||
childMap.put(head.getDescription(), newTreeItem);
|
||||
getChildren().add(newTreeItem);
|
||||
resort(comparator, false);
|
||||
return newTreeItem;
|
||||
});
|
||||
public void insert(Deque<EventStripe> path) {
|
||||
EventStripe head = path.removeFirst();
|
||||
|
||||
EventDescriptionTreeItem treeItem = childMap.computeIfAbsent(head.getDescription(),
|
||||
description -> {
|
||||
EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(head, comparator);
|
||||
newTreeItem.setExpanded(true);
|
||||
getChildren().add(newTreeItem);
|
||||
resort(comparator, false);
|
||||
return newTreeItem;
|
||||
});
|
||||
|
||||
if (path.isEmpty() == false) {
|
||||
treeItem.insert(path);
|
||||
}
|
||||
}
|
||||
|
||||
void remove(Deque<EventBundle<?>> path) {
|
||||
EventBundle<?> head = path.removeFirst();
|
||||
void remove(Deque<EventStripe> path) {
|
||||
EventStripe head = path.removeFirst();
|
||||
EventDescriptionTreeItem descTreeItem = childMap.get(head.getDescription());
|
||||
if (descTreeItem != null) {
|
||||
if (path.isEmpty() == false) {
|
||||
@ -78,11 +78,11 @@ class EventTypeTreeItem extends NavTreeItem {
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) {
|
||||
if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) {
|
||||
public EventsTreeItem findTreeItemForEvent(TimeLineEvent t) {
|
||||
if (t.getEventType().getBaseType() == eventType.getBaseType()) {
|
||||
|
||||
for (EventDescriptionTreeItem child : childMap.values()) {
|
||||
final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t);
|
||||
final EventsTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t);
|
||||
if (findTreeItemForEvent != null) {
|
||||
return findTreeItemForEvent;
|
||||
}
|
||||
@ -92,11 +92,22 @@ class EventTypeTreeItem extends NavTreeItem {
|
||||
}
|
||||
|
||||
@Override
|
||||
void resort(Comparator<TreeItem<EventBundle<?>>> comp, Boolean recursive) {
|
||||
void resort(Comparator<TreeItem<TimeLineEvent>> comp, Boolean recursive) {
|
||||
this.comparator = comp;
|
||||
FXCollections.sort(getChildren(), comp);
|
||||
if (recursive) {
|
||||
childMap.values().forEach(ti -> ti.resort(comp, true));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
String getDisplayText() {
|
||||
return eventType.getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
EventType getEventType() {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,7 +25,9 @@ import java.util.Objects;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
@ -43,13 +45,14 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.controlsfx.control.action.Action;
|
||||
import org.controlsfx.control.action.ActionUtils;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
|
||||
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.filters.AbstractFilter;
|
||||
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.detailview.DetailViewPane;
|
||||
|
||||
@ -66,42 +69,37 @@ final public class EventsTree extends BorderPane {
|
||||
private DetailViewPane detailViewPane;
|
||||
|
||||
@FXML
|
||||
private TreeView<EventBundle<?>> eventsTree;
|
||||
private TreeView<TimeLineEvent> eventsTree;
|
||||
|
||||
@FXML
|
||||
private Label eventsTreeLabel;
|
||||
|
||||
@FXML
|
||||
private ComboBox<TreeComparator> sortByBox;
|
||||
private final ObservableList<TimeLineEvent> selectedEvents = FXCollections.observableArrayList();
|
||||
|
||||
public EventsTree(TimeLineController controller) {
|
||||
this.controller = controller;
|
||||
|
||||
FXMLConstructor.construct(this, "EventsTree.fxml"); // NON-NLS
|
||||
}
|
||||
|
||||
public void setDetailViewPane(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
|
||||
while (c.next()) {
|
||||
for (EventBundle<?> bundle : c.getAddedSubList()) {
|
||||
getRoot().insert(bundle);
|
||||
}
|
||||
for (EventBundle<?> bundle : c.getRemoved()) {
|
||||
getRoot().remove(bundle);
|
||||
}
|
||||
c.getRemoved().forEach(getRoot()::remove);
|
||||
c.getAddedSubList().forEach(getRoot()::insert);
|
||||
}
|
||||
});
|
||||
|
||||
setRoot();
|
||||
|
||||
detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
|
||||
detailViewPane.getSelectedEvents().addListener((Observable observable) -> {
|
||||
eventsTree.getSelectionModel().clearSelection();
|
||||
detailViewPane.getSelectedNodes().forEach(eventBundleNode -> {
|
||||
eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventBundleNode.getEventBundle()));
|
||||
detailViewPane.getSelectedEvents().forEach(event -> {
|
||||
eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(event));
|
||||
});
|
||||
});
|
||||
|
||||
@ -114,11 +112,8 @@ final public class EventsTree extends BorderPane {
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
private void setRoot() {
|
||||
RootItem root = new RootItem(TreeComparator.Type.reversed().thenComparing(sortByBox.getSelectionModel().getSelectedItem()));
|
||||
for (EventBundle<?> bundle : detailViewPane.getEventStripes()) {
|
||||
root.insert(bundle);
|
||||
}
|
||||
detailViewPane.getAllEventStripes().forEach(root::insert);
|
||||
eventsTree.setRoot(root);
|
||||
|
||||
}
|
||||
|
||||
@FXML
|
||||
@ -130,28 +125,39 @@ final public class EventsTree extends BorderPane {
|
||||
sortByBox.getSelectionModel().select(TreeComparator.Description);
|
||||
sortByBox.setCellFactory(listView -> 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);
|
||||
});
|
||||
eventsTree.setShowRoot(false);
|
||||
|
||||
eventsTree.setShowRoot(false);
|
||||
eventsTree.setCellFactory(treeView -> new EventBundleTreeCell());
|
||||
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());
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
private class EventBundleTreeCell extends TreeCell<EventBundle<?>> {
|
||||
private class EventBundleTreeCell extends TreeCell<TimeLineEvent> {
|
||||
|
||||
private static final double HIDDEN_MULTIPLIER = .6;
|
||||
private final Rectangle rect = new Rectangle(24, 24);
|
||||
private final ImageView imageView = new ImageView();
|
||||
private InvalidationListener filterStateChangeListener;
|
||||
SimpleBooleanProperty hidden = new SimpleBooleanProperty(false);
|
||||
private final SimpleBooleanProperty hidden = new SimpleBooleanProperty(false);
|
||||
|
||||
EventBundleTreeCell() {
|
||||
rect.setArcHeight(5);
|
||||
@ -160,51 +166,42 @@ final public class EventsTree extends BorderPane {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(EventBundle<?> item, boolean empty) {
|
||||
protected void updateItem(TimeLineEvent item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item == null || empty) {
|
||||
if (empty) {
|
||||
setText(null);
|
||||
setTooltip(null);
|
||||
setGraphic(null);
|
||||
setContextMenu(null);
|
||||
deRegisterListeners(controller.getQuickHideFilters());
|
||||
} else {
|
||||
filterStateChangeListener = (filterState) -> updateHiddenState(item);
|
||||
controller.getQuickHideFilters().addListener((ListChangeListener.Change<? extends DescriptionFilter> listChange) -> {
|
||||
while (listChange.next()) {
|
||||
deRegisterListeners(listChange.getRemoved());
|
||||
registerListeners(listChange.getAddedSubList(), item);
|
||||
}
|
||||
updateHiddenState(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());
|
||||
}
|
||||
}
|
||||
EventsTreeItem treeItem = (EventsTreeItem) getTreeItem();
|
||||
String text = treeItem.getDisplayText();
|
||||
setText(text);
|
||||
setTooltip(new Tooltip(text));
|
||||
imageView.setImage(item.getEventType().getFXImage());
|
||||
|
||||
imageView.setImage(treeItem.getEventType().getFXImage());
|
||||
setGraphic(new StackPane(rect, imageView));
|
||||
updateHiddenState(item);
|
||||
if (getTreeItem() instanceof EventDescriptionTreeItem) {
|
||||
updateHiddenState(treeItem);
|
||||
deRegisterListeners(controller.getQuickHideFilters());
|
||||
|
||||
if (item != null) {
|
||||
filterStateChangeListener = (filterState) -> updateHiddenState(treeItem);
|
||||
controller.getQuickHideFilters().addListener((ListChangeListener.Change<? extends DescriptionFilter> listChange) -> {
|
||||
while (listChange.next()) {
|
||||
deRegisterListeners(listChange.getRemoved());
|
||||
registerListeners(listChange.getAddedSubList(), item);
|
||||
}
|
||||
updateHiddenState(treeItem);
|
||||
});
|
||||
registerListeners(controller.getQuickHideFilters(), item);
|
||||
setOnMouseClicked((MouseEvent event) -> {
|
||||
if (event.getButton() == MouseButton.SECONDARY) {
|
||||
if (hidden.get()) {
|
||||
ActionUtils.createContextMenu(ImmutableList.of(detailViewPane.newUnhideDescriptionAction(item.getDescription(), item.getDescriptionLoD())))
|
||||
.show(EventBundleTreeCell.this, event.getScreenX(), event.getScreenY());
|
||||
} else {
|
||||
ActionUtils.createContextMenu(ImmutableList.of(detailViewPane.newHideDescriptionAction(item.getDescription(), item.getDescriptionLoD())))
|
||||
.show(EventBundleTreeCell.this, event.getScreenX(), event.getScreenY());
|
||||
}
|
||||
Action action = hidden.get()
|
||||
? detailViewPane.newUnhideDescriptionAction(item.getDescription(), item.getDescriptionLoD())
|
||||
: detailViewPane.newHideDescriptionAction(item.getDescription(), item.getDescriptionLoD());
|
||||
|
||||
ActionUtils.createContextMenu(ImmutableList.of(action))
|
||||
.show(this, event.getScreenX(), event.getScreenY());
|
||||
}
|
||||
});
|
||||
} 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) {
|
||||
if (filter.getDescription().equals(item.getDescription())) {
|
||||
filter.activeProperty().addListener(filterStateChangeListener);
|
||||
@ -229,25 +226,22 @@ final public class EventsTree extends BorderPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateHiddenState(EventBundle<?> item) {
|
||||
TreeItem<EventBundle<?>> treeItem = getTreeItem();
|
||||
|
||||
hidden.set(controller.getQuickHideFilters().stream().
|
||||
filter(AbstractFilter::isActive)
|
||||
.anyMatch(filter -> filter.getDescription().equals(item.getDescription())));
|
||||
private void updateHiddenState(EventsTreeItem treeItem) {
|
||||
TimeLineEvent event = treeItem.getValue();
|
||||
hidden.set(event != null && controller.getQuickHideFilters().stream().
|
||||
filter(DescriptionFilter::isActive)
|
||||
.anyMatch(filter -> StringUtils.equalsIgnoreCase(filter.getDescription(), event.getDescription())));
|
||||
if (hidden.get()) {
|
||||
if (treeItem != null) {
|
||||
treeItem.setExpanded(false);
|
||||
}
|
||||
treeItem.setExpanded(false);
|
||||
setTextFill(Color.gray(0, HIDDEN_MULTIPLIER));
|
||||
imageView.setOpacity(HIDDEN_MULTIPLIER);
|
||||
rect.setStroke(item.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, 1, HIDDEN_MULTIPLIER));
|
||||
rect.setFill(item.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, HIDDEN_MULTIPLIER, 0.1));
|
||||
rect.setStroke(treeItem.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, 1, HIDDEN_MULTIPLIER));
|
||||
rect.setFill(treeItem.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, HIDDEN_MULTIPLIER, 0.1));
|
||||
} else {
|
||||
setTextFill(Color.BLACK);
|
||||
imageView.setOpacity(1);
|
||||
rect.setStroke(item.getEventType().getColor());
|
||||
rect.setFill(item.getEventType().getColor().deriveColor(0, 1, 1, 0.1));
|
||||
rect.setStroke(treeItem.getEventType().getColor());
|
||||
rect.setFill(treeItem.getEventType().getColor().deriveColor(0, 1, 1, 0.1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
|
||||
|
||||
import java.util.Comparator;
|
||||
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
|
||||
@ -28,12 +29,14 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
* {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type,
|
||||
* 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();
|
||||
|
||||
}
|
@ -26,13 +26,14 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import javafx.scene.control.TreeItem;
|
||||
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 RootItem extends NavTreeItem {
|
||||
class RootItem extends EventsTreeItem {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private Comparator<TreeItem<EventBundle<?>>> comparator = TreeComparator.Type.reversed();
|
||||
private Comparator<TreeItem<TimeLineEvent>> comparator = TreeComparator.Type.reversed();
|
||||
|
||||
RootItem(Comparator<TreeItem<EventBundle<?>>> comp) {
|
||||
comp = comp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCount() {
|
||||
return getValue().getCount();
|
||||
RootItem(Comparator<TreeItem<TimeLineEvent>> comp) {
|
||||
this.comparator = comp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
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 -> {
|
||||
EventTypeTreeItem newTreeItem = new EventTypeTreeItem(bundle, comparator);
|
||||
EventTypeTreeItem newTreeItem = new EventTypeTreeItem(stripe, comparator);
|
||||
newTreeItem.setExpanded(true);
|
||||
getChildren().add(newTreeItem);
|
||||
return newTreeItem;
|
||||
});
|
||||
treeItem.insert(getTreePath(bundle));
|
||||
|
||||
treeItem.insert(getTreePath(stripe));
|
||||
}
|
||||
|
||||
void remove(EventBundle<?> bundle) {
|
||||
EventTypeTreeItem typeTreeItem = childMap.get(bundle.getEventType().getBaseType());
|
||||
void remove(EventStripe stripe) {
|
||||
EventTypeTreeItem typeTreeItem = childMap.get(stripe.getEventType().getBaseType());
|
||||
if (typeTreeItem != null) {
|
||||
typeTreeItem.remove(getTreePath(bundle));
|
||||
typeTreeItem.remove(getTreePath(stripe));
|
||||
|
||||
if (typeTreeItem.getChildren().isEmpty()) {
|
||||
childMap.remove(bundle.getEventType().getBaseType());
|
||||
childMap.remove(stripe.getEventType().getBaseType());
|
||||
getChildren().remove(typeTreeItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Deque< EventBundle<?>> getTreePath(EventBundle<?> g) {
|
||||
Deque<EventBundle<?>> path = new ArrayDeque<>();
|
||||
Optional<? extends EventBundle<?>> p = Optional.of(g);
|
||||
|
||||
while (p.isPresent()) {
|
||||
EventBundle<?> parent = p.get();
|
||||
static Deque< EventStripe> getTreePath(EventStripe event) {
|
||||
Deque<EventStripe> path = new ArrayDeque<>();
|
||||
path.addFirst(event);
|
||||
Optional<EventStripe> parentOptional = event.getParentStripe();
|
||||
while (parentOptional.isPresent()) {
|
||||
EventStripe parent = parentOptional.get();
|
||||
path.addFirst(parent);
|
||||
p = parent.getParentBundle();
|
||||
parentOptional = parent.getParentStripe();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
void resort(Comparator<TreeItem<EventBundle<?>>> comp, Boolean recursive) {
|
||||
void resort(Comparator<TreeItem<TimeLineEvent>> comp, Boolean recursive) {
|
||||
comparator = comp;
|
||||
childMap.values().forEach(ti -> ti.resort(comp, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) {
|
||||
public EventsTreeItem findTreeItemForEvent(TimeLineEvent t) {
|
||||
for (EventTypeTreeItem child : childMap.values()) {
|
||||
final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t);
|
||||
final EventsTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t);
|
||||
if (findTreeItemForEvent != null) {
|
||||
return findTreeItemForEvent;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
String getDisplayText() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
EventType getEventType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,29 +21,29 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
|
||||
import java.util.Comparator;
|
||||
import javafx.scene.control.TreeItem;
|
||||
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;
|
||||
|
||||
@NbBundle.Messages({"TreeComparator.Description.displayName=Description",
|
||||
"TreeComparator.Count.displayName=Count",
|
||||
"TreeComparator.Type.displayName=Type"})
|
||||
enum TreeComparator implements Comparator<TreeItem<EventBundle<?>>> {
|
||||
enum TreeComparator implements Comparator<TreeItem<TimeLineEvent>> {
|
||||
|
||||
Description(Bundle.TreeComparator_Description_displayName()) {
|
||||
@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());
|
||||
}
|
||||
},
|
||||
Count(Bundle.TreeComparator_Count_displayName()) {
|
||||
@Override
|
||||
public int compare(TreeItem<EventBundle<?>> o1, TreeItem<EventBundle<?>> o2) {
|
||||
return Long.compare(o2.getValue().getCount(), o1.getValue().getCount());
|
||||
public int compare(TreeItem<TimeLineEvent> o1, TreeItem<TimeLineEvent> o2) {
|
||||
return Long.compare(o2.getValue().getSize(), o1.getValue().getSize());
|
||||
}
|
||||
},
|
||||
Type(Bundle.TreeComparator_Type_displayName()) {
|
||||
@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());
|
||||
}
|
||||
};
|
||||
|
@ -38,13 +38,14 @@ class FilterCheckBoxCellFactory<X extends AbstractFilter> extends AbstractFXCell
|
||||
checkBox.selectedProperty().unbindBidirectional(selectedProperty);
|
||||
}
|
||||
if (disabledProperty != null) {
|
||||
checkBox.disableProperty().unbind();//disabledProperty);
|
||||
checkBox.disableProperty().unbind();
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
cell.setGraphic(null);
|
||||
} else {
|
||||
checkBox.setText(item.getDisplayName());
|
||||
// cell.setText(item.getDisplayName());
|
||||
selectedProperty = item.selectedProperty();
|
||||
checkBox.selectedProperty().bindBidirectional(selectedProperty);
|
||||
disabledProperty = item.disabledProperty();
|
||||
|
@ -8,42 +8,56 @@
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<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">
|
||||
<items>
|
||||
<Button fx:id="applyButton" mnemonicParsing="false">
|
||||
<HBox.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</HBox.margin>
|
||||
<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/tick.png" />
|
||||
</image></ImageView>
|
||||
</graphic></Button><Button fx:id="defaultButton" mnemonicParsing="false">
|
||||
<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/arrow-circle-double-135.png" />
|
||||
</image></ImageView>
|
||||
</graphic></Button>
|
||||
</items>
|
||||
</ToolBar>
|
||||
</top>
|
||||
<center>
|
||||
<SplitPane fx:id="splitPane" dividerPositions="0.5" orientation="VERTICAL">
|
||||
<items>
|
||||
<TreeTableView fx:id="filterTreeTable" editable="true" minHeight="50.0" showRoot="false" BorderPane.alignment="CENTER">
|
||||
<columns>
|
||||
<TreeTableColumn fx:id="treeColumn" minWidth="100.0" prefWidth="200.0" sortable="false" />
|
||||
<TreeTableColumn fx:id="legendColumn" editable="false" minWidth="50.0" prefWidth="50.0" sortable="false" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TreeTableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TreeTableView>
|
||||
<TitledPane fx:id="hiddenDescriptionsPane" collapsible="false" disable="true" expanded="false" minHeight="25.0" text="Hidden Descriptions">
|
||||
<content>
|
||||
<ListView fx:id="hiddenDescriptionsListView" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
</items>
|
||||
</SplitPane>
|
||||
</center></fx:root>
|
||||
<top>
|
||||
<ToolBar prefWidth="200.0" BorderPane.alignment="CENTER">
|
||||
<items>
|
||||
<Button fx:id="applyButton" mnemonicParsing="false">
|
||||
<HBox.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</HBox.margin>
|
||||
<graphic>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/tick.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Button>
|
||||
<Button fx:id="defaultButton" mnemonicParsing="false">
|
||||
<graphic>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/arrow-circle-double-135.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Button>
|
||||
</items>
|
||||
</ToolBar>
|
||||
</top>
|
||||
<center>
|
||||
<SplitPane fx:id="splitPane" dividerPositions="0.5" orientation="VERTICAL">
|
||||
<items>
|
||||
<TreeTableView fx:id="filterTreeTable" editable="true" minHeight="50.0" showRoot="false" BorderPane.alignment="CENTER">
|
||||
<columns>
|
||||
<TreeTableColumn fx:id="treeColumn" minWidth="100.0" prefWidth="200.0" sortable="false" />
|
||||
<TreeTableColumn fx:id="legendColumn" editable="false" minWidth="50.0" prefWidth="50.0" sortable="false" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TreeTableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TreeTableView>
|
||||
<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>
|
||||
<ListView fx:id="hiddenDescriptionsListView" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
</items>
|
||||
</SplitPane>
|
||||
</center>
|
||||
</fx:root>
|
||||
|
@ -18,23 +18,20 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.ui.filtering;
|
||||
|
||||
import java.util.Arrays;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Cell;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.SplitPane;
|
||||
import javafx.scene.control.TitledPane;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableRow;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
@ -86,169 +83,71 @@ final public class FilterSetPanel extends BorderPane {
|
||||
@FXML
|
||||
private SplitPane splitPane;
|
||||
|
||||
private FilteredEventsModel filteredEvents;
|
||||
|
||||
private TimeLineController controller;
|
||||
private final FilteredEventsModel filteredEvents;
|
||||
private final TimeLineController controller;
|
||||
|
||||
private final ObservableMap<String, Boolean> expansionMap = FXCollections.observableHashMap();
|
||||
private double position;
|
||||
private double dividerPosition;
|
||||
|
||||
@FXML
|
||||
@NbBundle.Messages({
|
||||
"Timeline.ui.filtering.menuItem.all=all",
|
||||
"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"})
|
||||
@FXML
|
||||
void initialize() {
|
||||
assert applyButton != null : "fx:id=\"applyButton\" was not injected: check your FXML file 'FilterSetPanel.fxml'."; // NON-NLS
|
||||
|
||||
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());
|
||||
//remove column headers via css.
|
||||
filterTreeTable.getStylesheets().addAll(FilterSetPanel.class.getResource("FilterTable.css").toExternalForm()); // NON-NLS
|
||||
|
||||
//use row factory as hook to attach context menus to.
|
||||
filterTreeTable.setRowFactory((TreeTableView<Filter> param) -> {
|
||||
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;
|
||||
});
|
||||
filterTreeTable.setRowFactory(ttv -> new FilterTreeTableRow());
|
||||
|
||||
//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));
|
||||
|
||||
//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));
|
||||
|
||||
expansionMap.put(new TypeFilter(RootEventType.getInstance()).getDisplayName(), true);
|
||||
|
||||
Action defaultFiltersAction = new ResetFilters(controller);
|
||||
defaultButton.setOnAction(defaultFiltersAction);
|
||||
defaultButton.disableProperty().bind(defaultFiltersAction.disabledProperty());
|
||||
this.filteredEvents.eventTypeZoomProperty().addListener((Observable observable) -> applyFilters());
|
||||
this.filteredEvents.descriptionLODProperty().addListener((Observable observable1) -> applyFilters());
|
||||
this.filteredEvents.timeRangeProperty().addListener((Observable observable2) -> applyFilters());
|
||||
|
||||
this.filteredEvents.eventTypeZoomProperty().addListener((Observable observable) -> {
|
||||
applyFilters();
|
||||
});
|
||||
this.filteredEvents.descriptionLODProperty().addListener((Observable observable1) -> {
|
||||
applyFilters();
|
||||
});
|
||||
this.filteredEvents.timeRangeProperty().addListener((Observable observable2) -> {
|
||||
applyFilters();
|
||||
});
|
||||
this.filteredEvents.filterProperty().addListener((Observable o) -> {
|
||||
refresh();
|
||||
});
|
||||
this.filteredEvents.filterProperty().addListener((Observable o) -> refresh());
|
||||
refresh();
|
||||
|
||||
hiddenDescriptionsListView.setItems(controller.getQuickHideFilters());
|
||||
hiddenDescriptionsListView.setCellFactory((ListView<DescriptionFilter> param) -> {
|
||||
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;
|
||||
});
|
||||
hiddenDescriptionsListView.setCellFactory(listView -> getNewDiscriptionFilterListCell());
|
||||
|
||||
controller.viewModeProperty().addListener(observable -> {
|
||||
applyFilters();
|
||||
if (controller.viewModeProperty().get() == VisualizationMode.COUNTS) {
|
||||
position = splitPane.getDividerPositions()[0];
|
||||
dividerPosition = splitPane.getDividerPositions()[0];
|
||||
splitPane.setDividerPositions(1);
|
||||
hiddenDescriptionsPane.setExpanded(false);
|
||||
hiddenDescriptionsPane.setCollapsible(false);
|
||||
hiddenDescriptionsPane.setDisable(true);
|
||||
} else {
|
||||
splitPane.setDividerPositions(position);
|
||||
splitPane.setDividerPositions(dividerPosition);
|
||||
hiddenDescriptionsPane.setDisable(false);
|
||||
hiddenDescriptionsPane.setCollapsible(true);
|
||||
hiddenDescriptionsPane.setExpanded(true);
|
||||
hiddenDescriptionsPane.setCollapsible(false);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public FilterSetPanel(TimeLineController controller) {
|
||||
this.controller = controller;
|
||||
this.filteredEvents = controller.getEventsModel();
|
||||
FXMLConstructor.construct(this, "FilterSetPanel.fxml"); // NON-NLS
|
||||
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
@ -257,23 +156,52 @@ 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",
|
||||
"FilterSetPanel.applyButton.longText=(Re)Apply filters"})
|
||||
"FilterSetPanel.applyButton.longText=(Re)Apply filters"})
|
||||
private class ApplyFiltersAction extends Action {
|
||||
|
||||
ApplyFiltersAction() {
|
||||
super(Bundle.FilterSetPanel_applyButton_text());
|
||||
setLongText(Bundle.FilterSetPanel_applyButton_longText());
|
||||
setGraphic(new ImageView(TICK));
|
||||
setEventHandler((ActionEvent t) -> {
|
||||
applyFilters();
|
||||
});
|
||||
setEventHandler(actionEvent -> applyFilters());
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFilters() {
|
||||
Platform.runLater(() -> {
|
||||
controller.pushFilters((RootFilter) filterTreeTable.getRoot().getValue());
|
||||
});
|
||||
@NbBundle.Messages({
|
||||
"FilterSetPanel.hiddenDescriptionsListView.unhideAndRemove=Unhide and remove from list",
|
||||
"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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
115
Core/src/org/sleuthkit/autopsy/timeline/utils/MappedList.java
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2012-2014 Basis Technology Corp.
|
||||
* Copyright 2011-2016 Basis Technology Corp.
|
||||
*
|
||||
* Copyright 2012 42six Solutions.
|
||||
* Contact: aebadirad <at> 42six <dot> com
|
||||
@ -63,10 +63,10 @@ import org.sleuthkit.datamodel.*;
|
||||
class ExtractIE extends Extract {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ExtractIE.class.getName());
|
||||
private IngestServices services = IngestServices.getInstance();
|
||||
private String moduleTempResultsDir;
|
||||
private final IngestServices services = IngestServices.getInstance();
|
||||
private final String moduleTempResultsDir;
|
||||
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 Content dataSource;
|
||||
private IngestJobContext context;
|
||||
@ -126,7 +126,7 @@ class ExtractIE extends Extract {
|
||||
datetime = Long.valueOf(Tempdate);
|
||||
String domain = Util.extractDomain(url);
|
||||
|
||||
Collection<BlackboardAttribute> bbattributes = new ArrayList<BlackboardAttribute>();
|
||||
Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
|
||||
bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL,
|
||||
NbBundle.getMessage(this.getClass(),
|
||||
"ExtractIE.parentModuleName.noSpace"), url));
|
||||
@ -153,13 +153,15 @@ class ExtractIE extends Extract {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(new ReadContentInputStream(fav)));
|
||||
String line, url = "";
|
||||
try {
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = reader.readLine();
|
||||
while (null != line) {
|
||||
// The actual shortcut line we are interested in is of the
|
||||
// form URL=http://path/to/website
|
||||
if (line.startsWith("URL")) { //NON-NLS
|
||||
url = line.substring(line.indexOf("=") + 1);
|
||||
break;
|
||||
}
|
||||
line = reader.readLine();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
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);
|
||||
String domain = Util.extractDomain(url);
|
||||
|
||||
Collection<BlackboardAttribute> bbattributes = new ArrayList<BlackboardAttribute>();
|
||||
Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
|
||||
bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL,
|
||||
NbBundle.getMessage(this.getClass(),
|
||||
"ExtractIE.parentModuleName.noSpace"), url));
|
||||
@ -377,6 +379,15 @@ class ExtractIE extends Extract {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(commandLine);
|
||||
processBuilder.redirectOutput(new File(outputFileFullPath));
|
||||
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));
|
||||
// @@@ Investigate use of history versus cache as type.
|
||||
} catch (IOException ex) {
|
||||
@ -424,7 +435,7 @@ class ExtractIE extends Extract {
|
||||
}
|
||||
|
||||
// Keep a list of reported user accounts to avoid repeats
|
||||
Set<String> reportedUserAccounts = new HashSet<String>();
|
||||
Set<String> reportedUserAccounts = new HashSet<>();
|
||||
|
||||
while (fileScanner.hasNext()) {
|
||||
String line = fileScanner.nextLine();
|
||||
@ -439,12 +450,11 @@ class ExtractIE extends Extract {
|
||||
continue;
|
||||
}
|
||||
|
||||
String ddtime = lineBuff[2];
|
||||
String actime = lineBuff[3];
|
||||
Long ftime = (long) 0;
|
||||
String user = "";
|
||||
String realurl = "";
|
||||
String domain = "";
|
||||
String user;
|
||||
String realurl;
|
||||
String domain;
|
||||
|
||||
/*
|
||||
* We've seen two types of lines: URL http://XYZ.com .... URL
|
||||
@ -469,21 +479,15 @@ class ExtractIE extends Extract {
|
||||
|
||||
domain = Util.extractDomain(realurl);
|
||||
|
||||
if (!ddtime.isEmpty()) {
|
||||
ddtime = ddtime.replace("T", " "); //NON-NLS
|
||||
ddtime = ddtime.substring(ddtime.length() - 5);
|
||||
}
|
||||
|
||||
if (!actime.isEmpty()) {
|
||||
try {
|
||||
Long epochtime = dateFormatter.parse(actime).getTime();
|
||||
ftime = epochtime.longValue();
|
||||
ftime = ftime / 1000;
|
||||
ftime = epochtime / 1000;
|
||||
} catch (ParseException e) {
|
||||
this.addErrorMessage(
|
||||
NbBundle.getMessage(this.getClass(), "ExtractIE.parsePascoOutput.errMsg.errParsingEntry",
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,7 +220,7 @@ public class RegressionTest extends TestCase {
|
||||
JTableOperator jto = new JTableOperator(wo, 0);
|
||||
int row = jto.findCellRow("Hash Lookup", 1, 0);
|
||||
jto.clickOnCell(row, 1);
|
||||
JButtonOperator jbo1 = new JButtonOperator(wo, "Advanced");
|
||||
JButtonOperator jbo1 = new JButtonOperator(wo, "Global Settings");
|
||||
jbo1.pushNoBlock();
|
||||
}
|
||||
|
||||
@ -260,7 +260,7 @@ public class RegressionTest extends TestCase {
|
||||
JTableOperator jto = new JTableOperator(wo, 0);
|
||||
int row = jto.findCellRow("Keyword Search", 1, 0);
|
||||
jto.clickOnCell(row, 1);
|
||||
JButtonOperator jbo1 = new JButtonOperator(wo, "Advanced");
|
||||
JButtonOperator jbo1 = new JButtonOperator(wo, "Global Settings");
|
||||
jbo1.pushNoBlock();
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
#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
|
||||
SPLASH_HEIGHT=314
|
||||
SPLASH_WIDTH=538
|
||||
|
@ -1,4 +1,4 @@
|
||||
#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_No_Project=Autopsy 4.1.0
|
||||
|