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

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

View File

@ -604,7 +604,7 @@ public class Case implements SleuthkitCase.ErrorObserver {
SleuthkitCase db;
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);
}

View File

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

View File

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

View File

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

View 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)

View File

@ -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() {

View File

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

View File

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

View File

@ -44,7 +44,8 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType;
import org.sleuthkit.autopsy.timeline.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;
}

View File

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

View File

@ -0,0 +1,187 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2014-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.datamodel;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.Set;
import java.util.SortedSet;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.joda.time.Interval;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
import org.sleuthkit.datamodel.TskData;
/**
* A single event.
*/
@Immutable
public class SingleEvent implements TimeLineEvent {
private final long eventID;
private final long fileID;
private final Long artifactID;
private final long dataSourceID;
private final long time;
private final EventType subType;
private final ImmutableMap<DescriptionLoD, String> descriptions;
private final TskData.FileKnown known;
private final boolean hashHit;
private final boolean tagged;
public SingleEvent(long eventID, long dataSourceID, long objID, @Nullable Long artifactID, long time, EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, boolean hashHit, boolean tagged) {
this.eventID = eventID;
this.fileID = objID;
this.artifactID = artifactID == 0 ? null : artifactID;
this.time = time;
this.subType = type;
descriptions = ImmutableMap.<DescriptionLoD, String>of(DescriptionLoD.FULL, fullDescription,
DescriptionLoD.MEDIUM, medDescription,
DescriptionLoD.SHORT, shortDescription);
this.known = known;
this.hashHit = hashHit;
this.tagged = tagged;
this.dataSourceID = dataSourceID;
}
public boolean isTagged() {
return tagged;
}
public boolean isHashHit() {
return hashHit;
}
@Nullable
public Long getArtifactID() {
return artifactID;
}
public long getEventID() {
return eventID;
}
public long getFileID() {
return fileID;
}
/**
* @return the time in seconds from unix epoch
*/
public long getTime() {
return time;
}
public EventType getEventType() {
return subType;
}
public String getFullDescription() {
return getDescription(DescriptionLoD.FULL);
}
public String getMedDescription() {
return getDescription(DescriptionLoD.MEDIUM);
}
public String getShortDescription() {
return getDescription(DescriptionLoD.SHORT);
}
public TskData.FileKnown getKnown() {
return known;
}
public String getDescription(DescriptionLoD lod) {
return descriptions.get(lod);
}
public long getDataSourceID() {
return dataSourceID;
}
@Override
public Set<Long> getEventIDs() {
return Collections.singleton(eventID);
}
@Override
public Set<Long> getEventIDsWithHashHits() {
return isHashHit() ? Collections.singleton(eventID) : Collections.emptySet();
}
@Override
public Set<Long> getEventIDsWithTags() {
return isTagged() ? Collections.singleton(eventID) : Collections.emptySet();
}
@Override
public long getEndMillis() {
return time * 1000;
}
@Override
public long getStartMillis() {
return time * 1000;
}
@Override
public int hashCode() {
int hash = 7;
hash = 13 * hash + (int) (this.eventID ^ (this.eventID >>> 32));
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final SingleEvent other = (SingleEvent) obj;
if (this.eventID != other.eventID) {
return false;
}
return true;
}
@Override
public SortedSet<EventCluster> getClusters() {
EventCluster eventCluster = new EventCluster(new Interval(time * 1000, time * 1000), subType, getEventIDs(), getEventIDsWithHashHits(), getEventIDsWithTags(), getFullDescription(), DescriptionLoD.FULL);
return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(eventCluster).build();
}
@Override
public String getDescription() {
return getFullDescription();
}
@Override
public DescriptionLoD getDescriptionLoD() {
return DescriptionLoD.FULL;
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* 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();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -26,16 +26,14 @@ import java.util.Set;
* been(un)tagged. This event is not intended for use out side of the timeline
* 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

View File

@ -45,8 +45,6 @@ import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.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);

View File

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

View File

@ -1,12 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<?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>

View File

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

View File

@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,121 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2016 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
/**
*
*/
public class DetailsChartLayoutSettings {
/**
* true == truncate all the labels to the greater of the size of their
* timespan indicator or the value of truncateWidth. false == don't truncate
* the labels, alow them to extend past the timespan indicator and off the
* edge of the screen
*/
final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false);
/**
* the width to truncate all labels to if truncateAll is true. adjustable
* via slider if truncateAll is true
*/
final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
/**
* true == layout each event type in its own band, false == mix all the
* events together during layout
*/
private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false);
/**
* true == enforce that no two events can share the same 'row', leading to
* sparser but possibly clearer layout. false == put unrelated events in the
* same 'row', creating a denser more compact layout
*/
private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false);
/**
* how much detail of the description to show in the ui
*/
private final SimpleObjectProperty<DescriptionVisibility> descrVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN);
/**
* is the pinned events lane showing
*/
private final SimpleBooleanProperty pinnedLaneShowing = new SimpleBooleanProperty(false);
public synchronized SimpleBooleanProperty bandByTypeProperty() {
return bandByType;
}
public SimpleBooleanProperty pinnedLaneShowing() {
return pinnedLaneShowing;
}
public boolean isPinnedLaneShowing() {
return pinnedLaneShowing.get();
}
public void setPinnedLaneShowing(boolean showing) {
pinnedLaneShowing.set(showing);
}
SimpleBooleanProperty oneEventPerRowProperty() {
return oneEventPerRow;
}
SimpleDoubleProperty truncateWidthProperty() {
return truncateWidth;
}
SimpleBooleanProperty truncateAllProperty() {
return truncateAll;
}
SimpleObjectProperty< DescriptionVisibility> descrVisibilityProperty() {
return descrVisibility;
}
synchronized void setBandByType(Boolean t1) {
bandByType.set(t1);
}
boolean getBandByType() {
return bandByType.get();
}
boolean getTruncateAll() {
return truncateAll.get();
}
double getTruncateWidth() {
return truncateWidth.get();
}
boolean getOneEventPerRow() {
return oneEventPerRow.get();
}
DescriptionVisibility getDescrVisibility() {
return descrVisibility.get();
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* 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.
}

View File

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

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* 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()));
}
}
}

View File

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

View File

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

View File

@ -1,8 +1,7 @@
/*
* Autopsy Forensic Browser
*
* 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())
);
}
}

View File

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

View File

@ -0,0 +1,129 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import java.util.List;
import java.util.Set;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import org.joda.time.DateTime;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
/**
*
*/
@NbBundle.Messages({"EventBundleNodeBase.toolTip.loading=loading..."})
public abstract class MultiEventNodeBase< BundleType extends MultiEvent<ParentType>, ParentType extends MultiEvent<BundleType>, ParentNodeType extends MultiEventNodeBase<
ParentType, BundleType, ?>> extends EventNodeBase<BundleType> {
private static final Logger LOGGER = Logger.getLogger(MultiEventNodeBase.class.getName());
static final CornerRadii CORNER_RADII_3 = new CornerRadii(3);
static final CornerRadii CORNER_RADII_1 = new CornerRadii(1);
final ObservableList<EventNodeBase<?>> subNodes = FXCollections.observableArrayList();
final Pane subNodePane = new Pane();
private final ReadOnlyObjectWrapper<DescriptionLoD> descLOD = new ReadOnlyObjectWrapper<>();
MultiEventNodeBase(DetailsChartLane<?> chartLane, BundleType event, ParentNodeType parentNode) {
super(event, parentNode, chartLane);
setDescriptionLOD(event.getDescriptionLoD());
setAlignment(Pos.TOP_LEFT);
setMaxWidth(USE_PREF_SIZE);
infoHBox.setMaxWidth(USE_PREF_SIZE);
subNodePane.setPrefWidth(USE_COMPUTED_SIZE);
subNodePane.setMinWidth(USE_PREF_SIZE);
subNodePane.setMaxWidth(USE_PREF_SIZE);
/*
* This triggers the layout when a mousover causes the action buttons to
* interesect with another node, forcing it down.
*/
heightProperty().addListener(heightProp -> chartLane.requestLayout());
Platform.runLater(() ->
setLayoutX(chartLane.getXAxis().getDisplayPosition(new DateTime(event.getStartMillis())) - getLayoutXCompensation())
);
//initialize info hbox
infoHBox.setPadding(new Insets(2, 3, 2, 3));
infoHBox.setAlignment(Pos.TOP_LEFT);
Bindings.bindContent(subNodePane.getChildren(), subNodes);
}
public ReadOnlyObjectProperty<DescriptionLoD> descriptionLoDProperty() {
return descLOD.getReadOnlyProperty();
}
final DescriptionLoD getDescriptionLoD() {
return descLOD.get();
}
/**
*
*/
final void setDescriptionLOD(final DescriptionLoD descriptionLoD) {
descLOD.set(descriptionLoD);
}
@SuppressWarnings("unchecked")
public List<EventNodeBase<?>> getSubNodes() {
return subNodes;
}
final String getDescription() {
return getEvent().getDescription();
}
final Set<Long> getEventIDs() {
return getEvent().getEventIDs();
}
@Override
public Orientation getContentBias() {
return Orientation.HORIZONTAL;
}
@Override
protected void layoutChildren() {
chartLane.layoutEventBundleNodes(subNodes, 0);
super.layoutChildren();
}
abstract EventNodeBase<?> createChildNode(ParentType rawChild);
abstract EventHandler<MouseEvent> getDoubleClickHandler();
}

View File

@ -0,0 +1,61 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2016 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import javafx.collections.SetChangeListener;
import javafx.scene.chart.Axis;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
/**
*
*/
public final class PinnedEventsChartLane extends DetailsChartLane<TimeLineEvent> {
/**
*
* @param controller the value of controller
* @param dateAxis the value of dateAxis
* @param verticalAxis the value of verticalAxis
* @param selectedNodes1 the value of selectedNodes1
*/
PinnedEventsChartLane(DetailsChart parentChart, DateAxis dateAxis, final Axis<TimeLineEvent> verticalAxis) {
super(parentChart, dateAxis, verticalAxis, false);
// final Series<DateTime, TimeLineEvent> series = new Series<>();
// setData(FXCollections.observableArrayList());
// getData().add(series);
getController().getPinnedEvents().addListener((SetChangeListener.Change<? extends TimeLineEvent> change) -> {
if (change.wasAdded()) {
addEvent(change.getElementAdded());
}
if (change.wasRemoved()) {
removeEvent(change.getElementRemoved());
}
requestChartLayout();
});
getController().getPinnedEvents().stream().forEach(this::addEvent);
requestChartLayout();
}
@Override
void doAdditionalLayout() {
}
}

View File

@ -0,0 +1,115 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2016 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javafx.collections.ListChangeListener;
import javafx.scene.chart.Axis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.shape.Line;
import javafx.scene.shape.StrokeLineCap;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider;
/**
* Custom implementation of {@link XYChart} to graph events on a horizontal
* timeline.
*
* The horizontal {@link DateAxis} controls the tick-marks and the horizontal
* layout of the nodes representing events. The vertical {@link NumberAxis} does
* nothing (although a custom implementation could help with the vertical
* layout?)
*
* Series help organize events for the banding by event type, we could add a
* node to contain each band if we need a place for per band controls.
*
* //TODO: refactor the projected lines to a separate class. -jm
*/
public final class PrimaryDetailsChartLane extends DetailsChartLane<EventStripe> implements ContextMenuProvider {
private static final int PROJECTED_LINE_Y_OFFSET = 5;
private static final int PROJECTED_LINE_STROKE_WIDTH = 5;
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final Map<EventCluster, Line> projectionMap = new ConcurrentHashMap<>();
PrimaryDetailsChartLane(DetailsChart parentChart, DateAxis dateAxis, final Axis<EventStripe> verticalAxis) {
super(parentChart, dateAxis, verticalAxis, true);
//add listener for events that should trigger layout
getController().getQuickHideFilters().addListener(layoutInvalidationListener);
parentChart.getRootEventStripes().addListener((ListChangeListener.Change<? extends EventStripe> change) -> {
while (change.next()) {
change.getAddedSubList().stream().forEach(this::addEvent);
change.getRemoved().stream().forEach(this::removeEvent);
}
requestChartLayout();
});
parentChart.getRootEventStripes().stream().forEach(this::addEvent);
requestChartLayout();
getSelectedNodes().addListener((ListChangeListener.Change<? extends EventNodeBase<?>> change) -> {
while (change.next()) {
change.getRemoved().forEach(removedNode -> {
removedNode.getEvent().getClusters().forEach(cluster -> {
Line removedLine = projectionMap.remove(cluster);
getChartChildren().removeAll(removedLine);
});
});
change.getAddedSubList().forEach(addedNode -> {
for (EventCluster range : addedNode.getEvent().getClusters()) {
double y = dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET;
Line line =
new Line(dateAxis.localToParent(getXForEpochMillis(range.getStartMillis()), 0).getX(), y,
dateAxis.localToParent(getXForEpochMillis(range.getEndMillis()), 0).getX(), y);
line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5));
line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
line.setStrokeLineCap(StrokeLineCap.ROUND);
projectionMap.put(range, line);
getChartChildren().add(line);
}
});
}
});
}
private double getParentXForEpochMillis(Long epochMillis) {
return getXAxis().localToParent(getXForEpochMillis(epochMillis), 0).getX();
}
void doAdditionalLayout() {
for (final Map.Entry<EventCluster, Line> entry : projectionMap.entrySet()) {
final EventCluster cluster = entry.getKey();
final Line line = entry.getValue();
line.setStartX(getParentXForEpochMillis(cluster.getStartMillis()));
line.setEndX(getParentXForEpochMillis(cluster.getEndMillis()));
line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
}
}
}

View File

@ -0,0 +1,105 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2016 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.geometry.Orientation;
import javafx.scene.control.ScrollBar;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
/**
*
*/
class ScrollingLaneWrapper extends BorderPane {
private static final double LINE_SCROLL_PERCENTAGE = .10;
private static final double PAGE_SCROLL_PERCENTAGE = .70;
private final ScrollBar vertScrollBar = new ScrollBar();
private final Region scrollBarSpacer = new Region();
private final DetailsChartLane<?> chart;
ScrollingLaneWrapper(DetailsChartLane<?> center) {
super(center);
this.chart = center;
scrollBarSpacer.minHeightProperty().bind(chart.getXAxis().heightProperty());
//configure scrollbar
vertScrollBar.setOrientation(Orientation.VERTICAL);
vertScrollBar.maxProperty().bind(chart.maxVScrollProperty().subtract(chart.heightProperty()));
vertScrollBar.visibleAmountProperty().bind(chart.heightProperty());
vertScrollBar.visibleProperty().bind(vertScrollBar.visibleAmountProperty().greaterThanOrEqualTo(0));
VBox.setVgrow(vertScrollBar, Priority.ALWAYS);
setRight(new VBox(vertScrollBar, scrollBarSpacer));
//scrollbar value change handler. This forwards changes in scroll bar to chart
this.vertScrollBar.valueProperty().addListener((Observable observable) -> {
chart.setVScroll(vertScrollBar.getValue());
});
//request focus for keyboard scrolling
setOnMouseClicked(mouseEvent -> requestFocus());
//interpret scroll events to the scrollBar
this.setOnScroll(scrollEvent ->
vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() - scrollEvent.getDeltaY())));
//interpret scroll related keys to scrollBar
this.setOnKeyPressed((KeyEvent t) -> {
switch (t.getCode()) {
case PAGE_UP:
incrementScrollValue(-PAGE_SCROLL_PERCENTAGE);
t.consume();
break;
case PAGE_DOWN:
incrementScrollValue(PAGE_SCROLL_PERCENTAGE);
t.consume();
break;
case KP_UP:
case UP:
incrementScrollValue(-LINE_SCROLL_PERCENTAGE);
t.consume();
break;
case KP_DOWN:
case DOWN:
incrementScrollValue(LINE_SCROLL_PERCENTAGE);
t.consume();
break;
}
});
}
void reset() {
Platform.runLater(() -> {
vertScrollBar.setValue(0);
});
}
private void incrementScrollValue(double factor) {
vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() + factor * chart.getHeight()));
}
private Double clampScroll(Double value) {
return Math.max(0, Math.min(vertScrollBar.getMax() + 50, value));
}
}

View File

@ -0,0 +1,103 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2016 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.OverrunStyle;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
/**
*
*/
final class SingleEventNode extends EventNodeBase<SingleEvent> {
private static final Logger LOGGER = Logger.getLogger(SingleEventNode.class.getName());
static void show(Node b, boolean show) {
b.setVisible(show);
b.setManaged(show);
}
static final CornerRadii CORNER_RADII_1 = new CornerRadii(1);
private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(0, 0, 0, 2);
@Override
EventHandler<MouseEvent> getDoubleClickHandler() {
return mouseEvent -> {
};
}
SingleEventNode(DetailsChartLane<?> chart, SingleEvent event, MultiEventNodeBase<?, ?, ?> parent) {
super(event, parent, chart);
this.descrLabel.setText(event.getFullDescription());
eventTypeImageView.setImage(getEventType().getFXImage());
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
descrLabel.setGraphic(eventTypeImageView);
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
setMinHeight(24);
setAlignment(Pos.CENTER_LEFT);
final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS));
setBorder(clusterBorder);
setMaxWidth(USE_PREF_SIZE);
infoHBox.setMaxWidth(USE_PREF_SIZE);
getChildren().add(infoHBox);
}
@Override
public List<EventNodeBase<?>> getSubNodes() {
return Collections.emptyList();
}
@Override
protected void layoutChildren() {
super.layoutChildren(); //To change body of generated methods, choose Tools | Templates.
}
@Override
String getDescription() {
return getEvent().getFullDescription();
}
/**
* @param w the maximum width the description label should have
*/
@Override
public void setMaxDescriptionWidth(double w) {
descrLabel.setMaxWidth(w);
}
@Override
Collection<Long> getEventIDs() {
return getEvent().getEventIDs();
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* 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)));
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
*
* Autopsy Forensic Browser
*
* 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
}
}

View File

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

View File

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

View File

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