From 908a6b640e890c8c9fa641a9572476a3bc3c3168 Mon Sep 17 00:00:00 2001 From: Eugene Livis Date: Fri, 8 Apr 2016 10:48:59 -0400 Subject: [PATCH 01/35] Treating Pasco output parsing error as warning + note on Pasco return codes --- .../sleuthkit/autopsy/recentactivity/ExtractIE.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java index 627e32a46f..3ed41231f7 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java @@ -377,6 +377,14 @@ 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) { @@ -483,7 +491,7 @@ class ExtractIE extends Extract { 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, "Error parsing Pasco results.", e); //NON-NLS } } From d241e0882be59ac592110ca9fd5a84d4cada26c8 Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Fri, 8 Apr 2016 12:01:17 -0400 Subject: [PATCH 02/35] casemodule/CaseMetadata: Record software version, refine API --- .../sleuthkit/autopsy/casemodule/Case.java | 4 +- .../autopsy/casemodule/CaseMetadata.java | 119 ++++++++++-------- .../casemodule/CasePropertiesForm.java | 6 +- .../casemodule/SingleUserCaseConverter.java | 2 +- 4 files changed, 72 insertions(+), 59 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index f871fff696..2d1d0c0a03 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -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); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java index bf0862ba02..bd07a3deaf 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java @@ -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(); - } - } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesForm.java b/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesForm.java index 2f024e724a..537cd0f3d2 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesForm.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesForm.java @@ -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) { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseConverter.java b/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseConverter.java index 458cd642dc..95d94d1f23 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseConverter.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseConverter.java @@ -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 From 0db3a8ba2cd203328c417fb394035476228526ec Mon Sep 17 00:00:00 2001 From: jmillman Date: Thu, 18 Feb 2016 16:58:06 -0500 Subject: [PATCH 03/35] experimental/preliminary work to support pinning events in the detail view more refactoring and development to support pinning events move more into EventNodeBase.java more refactoring, and enhancement of event pinning --- .../autopsy/timeline/TimeLineController.java | 16 + .../autopsy/timeline/datamodel/Event.java | 40 ++ .../timeline/datamodel/EventCluster.java | 5 +- .../timeline/datamodel/EventStripe.java | 4 +- .../datamodel/FilteredEventsModel.java | 4 +- .../{EventBundle.java => MultiEvent.java} | 2 +- .../{TimeLineEvent.java => SingleEvent.java} | 75 +++- .../autopsy/timeline/db/EventDB.java | 10 +- .../autopsy/timeline/db/EventsRepository.java | 8 +- .../timeline/explorernodes/EventNode.java | 20 +- .../timeline/explorernodes/EventRootNode.java | 6 +- .../autopsy/timeline/images/marker--arrow.png | Bin 0 -> 672 bytes .../timeline/images/marker--exclamation.png | Bin 0 -> 695 bytes .../autopsy/timeline/images/marker--minus.png | Bin 0 -> 567 bytes .../timeline/images/marker--pencil.png | Bin 0 -> 660 bytes .../autopsy/timeline/images/marker--pin.png | Bin 0 -> 497 bytes .../autopsy/timeline/images/marker--plus.png | Bin 0 -> 656 bytes .../ui/AbstractVisualizationPane.java | 5 +- .../autopsy/timeline/ui/TimeLineChart.java | 5 + .../ui/countsview/EventCountsChart.java | 2 + .../timeline/ui/detailview/DateAxis.java | 2 + .../ui/detailview/DetailViewPane.java | 67 +-- .../timeline/ui/detailview/DetailsChart.java | 80 ++++ .../timeline/ui/detailview/EventAxis.java | 18 +- .../ui/detailview/EventClusterNode.java | 73 ++-- .../ui/detailview/EventDetailsChart.java | 120 +++--- .../timeline/ui/detailview/EventNodeBase.java | 258 ++++++++++++ .../ui/detailview/EventStripeNode.java | 23 +- ...eNodeBase.java => MultiEventNodeBase.java} | 162 ++------ .../ui/detailview/PinnedEventsChart.java | 380 ++++++++++++++++++ .../ui/detailview/SingleEventNode.java | 202 ++++++++++ .../ui/detailview/StripeFlattener.java | 8 +- .../tree/EventDescriptionTreeItem.java | 23 +- .../ui/detailview/tree/EventTypeTreeItem.java | 19 +- .../ui/detailview/tree/EventsTree.java | 29 +- .../ui/detailview/tree/NavTreeItem.java | 8 +- .../timeline/ui/detailview/tree/RootItem.java | 25 +- .../ui/detailview/tree/TreeComparator.java | 10 +- 38 files changed, 1343 insertions(+), 366 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/datamodel/Event.java rename Core/src/org/sleuthkit/autopsy/timeline/datamodel/{EventBundle.java => MultiEvent.java} (94%) rename Core/src/org/sleuthkit/autopsy/timeline/datamodel/{TimeLineEvent.java => SingleEvent.java} (57%) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/images/marker--arrow.png create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/images/marker--exclamation.png create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/images/marker--minus.png create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/images/marker--pencil.png create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/images/marker--pin.png create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/images/marker--plus.png create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java rename Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/{EventBundleNodeBase.java => MultiEventNodeBase.java} (67%) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index c77ba8ee75..af1198d6f5 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -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; @@ -290,6 +291,21 @@ public class TimeLineController { advance(filteredEvents.zoomParametersProperty().get().withTimeRange(boundingEventsInterval)); } + private final ObservableSet pinnedEventIDs = FXCollections.observableSet(); + private final ObservableSet pinnedEventIDsUnmodifiable = FXCollections.unmodifiableObservableSet(pinnedEventIDs); + + public void pinEvents(Collection eventIDs) { + pinnedEventIDs.addAll(eventIDs); + } + + public void unPinEvents(Collection eventIDs) { + pinnedEventIDs.removeAll(eventIDs); + } + + public ObservableSet getPinnedEventIDs() { + return pinnedEventIDsUnmodifiable; + } + /** * rebuild the repo using the given repo builder (expected to be a member * reference to {@link EventsRepository#rebuildRepository(java.util.function.Consumer) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/Event.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/Event.java new file mode 100644 index 0000000000..6f8e47dd94 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/Event.java @@ -0,0 +1,40 @@ +/* + * 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.datamodel; + +import java.util.Set; +import java.util.SortedSet; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; + +/** + * + */ +public interface Event { + + public String getDescription(); + + public DescriptionLoD getDescriptionLoD(); + + Set getEventIDs(); + + Set getEventIDsWithHashHits(); + + Set getEventIDsWithTags(); + + EventType getEventType(); + + long getEndMillis(); + + long getStartMillis(); + + default int getCount() { + return getEventIDs().size(); + } + + + SortedSet getClusters(); +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index ac5a02d007..c353979e3a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -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 + * 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 { +public class EventCluster implements MultiEvent { /** * merge two event clusters into one new event cluster. @@ -192,4 +192,5 @@ public class EventCluster implements EventBundle { return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(this).build(); } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index ffd8338cf0..a9d8a6870a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -34,7 +34,7 @@ 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 { +public final class EventStripe implements MultiEvent { public static EventStripe merge(EventStripe u, EventStripe v) { Preconditions.checkNotNull(u); @@ -193,4 +193,6 @@ public final class EventStripe implements EventBundle { public String toString() { return "EventStripe{" + "description=" + description + ", eventIDs=" + eventIDs.size() + '}'; //NON-NLS } + + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index bf161241b1..05401511a9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -232,11 +232,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 getEventsById(Collection eventIDs) { + public Set getEventsById(Collection eventIDs) { return repo.getEventsById(eventIDs); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java similarity index 94% rename from Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java rename to Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java index 7361bf5d88..1c1ceaa73c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java @@ -27,7 +27,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * A interface for groups of events that share some attributes in common. */ -public interface EventBundle> { +public interface MultiEvent> extends Event { String getDescription(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/SingleEvent.java similarity index 57% rename from Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java rename to Core/src/org/sleuthkit/autopsy/timeline/datamodel/SingleEvent.java index 2296edfbf8..b806fdfca4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/SingleEvent.java @@ -19,8 +19,14 @@ 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; @@ -29,7 +35,7 @@ import org.sleuthkit.datamodel.TskData; * A single event. */ @Immutable -public class TimeLineEvent { +public class SingleEvent implements Event { private final long eventID; private final long fileID; @@ -44,7 +50,7 @@ public class TimeLineEvent { private final boolean hashHit; private final boolean tagged; - public TimeLineEvent(long eventID, long dataSourceID, long objID, @Nullable Long artifactID, long time, EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, boolean hashHit, boolean tagged) { + 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; @@ -88,7 +94,7 @@ public class TimeLineEvent { return time; } - public EventType getType() { + public EventType getEventType() { return subType; } @@ -115,4 +121,67 @@ public class TimeLineEvent { public long getDataSourceID() { return dataSourceID; } + + @Override + public Set getEventIDs() { + return Collections.singleton(eventID); + } + + @Override + public Set getEventIDsWithHashHits() { + return isHashHit() ? Collections.singleton(eventID) : Collections.emptySet(); + } + + @Override + public Set 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 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; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index ed0357d6fa..246fee2226 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java @@ -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 diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index 0583b0c175..fe770ce77a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -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 maxCache; private final LoadingCache minCache; - private final LoadingCache idToEventCache; + private final LoadingCache idToEventCache; private final LoadingCache> eventCountsCache; private final LoadingCache> 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 getEventsById(Collection eventIDs) { + synchronized public Set getEventsById(Collection eventIDs) { return eventIDs.stream() .map(idToEventCache::getUnchecked) .collect(Collectors.toSet()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java index 04b0370b42..abb7245eda 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java @@ -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 diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java index 9ad3a2bb2f..e84c49922f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java @@ -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()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/marker--arrow.png b/Core/src/org/sleuthkit/autopsy/timeline/images/marker--arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..e9c33c570fce3ca96161e678e39290287d6ce16c GIT binary patch literal 672 zcmV;R0$=@!P)sh^s^i~2rXbB>z#ZtkF!9$9ORD+0!@uy{_ zwuz9nWs~h@V|Jf2yKcxXwgVsY=9@PkGxHwD7z4+)d7fMsB7r!GXh&2L9}x2h89e6q z#65^xfzi>(p;)ZX*VYy^biJ%*v&SkcD-q01BMJuz#DfzP{lS5OD3fKN-ms@&$MEne zuBF9aUS5tPb+@^IZw?G>TNoS+o8>a3QQ(RsS)O9zDR0>K8gL2JaAuC@1!frVq*#Q4 zqFA0{;wfLV?=|2O&;csCEbmgm89@ME(=1Oh@mAix*MMsWxb8Q_)zuS+W3f=8yBi2( zthE)2*{nqQ1786zEEaOPv5+W++c~bo;k8OdLH;Km7k?Z*4au92LC5a)a5vs1mNdqe zzGpJu!0!jMUI+3NTYTS0iHi4q1qDAf)FUD^Ge!L{&;(rA+ z8!C>nkY8Kd1dappUylqe%w4;$e(5<5wQ5yFOgSQ4^H)w`!9NQk`~3XmM4X literal 0 HcmV?d00001 diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/marker--exclamation.png b/Core/src/org/sleuthkit/autopsy/timeline/images/marker--exclamation.png new file mode 100644 index 0000000000000000000000000000000000000000..b225a567b382355128ef59bf63c2ebc2ea84624f GIT binary patch literal 695 zcmV;o0!aOdP)X( zGJS~B8ef#OwY9C{Vk*`}Z7FVCltK~x3@*gAxX_PK5L_k{r7B%ym8>L?g`iN;ltA;c zI#n_gl1VbTo;$H26KF5|IGo4*&$(yrC6rP)T1AdyCX8$#lZc4ak+hJAZxL5P#PwefJ}g_zpY| z?N$qz&88ArZqa34Q8(uZ;TQ1gi0t|>90C=L)G3V>v)O&EP&i~~XZCPlkA~*%xJ1#f z1eJxWRvYfN@M{c*K&?X+j}qTar}u#n_?AiM-7BOqaPIbnwTCeX58b-dw#a>}1w7&u zsQ(A2`@WFRS1mk(7B29?sRTN)k5udB`D1P~oXToeU_8*nB#933tG zNyEnvPvBR57lhDY1XB;1O}rkx0!=4eMLq!O;%SNUg|M5`V0q;wNP-SN$qWmkZxTgr zIs|ARMI{R((V*uPR|20k@7DNO=`~yNA930$SogD;!f{Pg(Dx%b(gHxSE=pZgWq0lE{iZ-pW)%5e+)RcU& zUN}A6N6vS?n??~rkP|soW$Fhazy#0%d;&Ls2I$W0;{s7?Ns!_Z7zFKhFE5XEg~Ox1 z_V$ofts0d?;=UY@-=enzc}t_ew?hC3^&8WfgAr-cILOC_5*=Q{UmjkzWy zfhtkCs_Sq3z+x~+s%hHHIoIueW3CBl2Uz!2;qr2PC>-vK3=9y1Lj(d;NF-9+_v}n| zfPs^IGMV`(mrH!A1J1e5edlBYc)%O7{9My z5-6egDMU2){G8Ua*&?6L%sJO}^ou~v_1}>M%mym3dVF_%ohC&gX3n|(MSsJ)L?_O( znaq}jD}XB)T2Y9$y${1j@;?FIkxxJX3-jULBkPgKj91`L(_{x8iz@E`HMNM7_B5@P zec6HVTCCTd?_twyasSR1ACo%{j-pYUd-3!B&(Mzm0|0im4AhcuK!X4P002ovPDHLk FV1mN&0+Ij# literal 0 HcmV?d00001 diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/marker--pencil.png b/Core/src/org/sleuthkit/autopsy/timeline/images/marker--pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..c18784e7cf00714b60bb72d00af053214a58fa13 GIT binary patch literal 660 zcmV;F0&D$=P)xv&t%*aJlNe+5RBp`nXPclQOM>p;C>Ps-80 zz8=Zvla2ZLQFJ}47qEJRW7~n=-VVnwAc_TUCK4`_O@4~=YMzTdZ-Fv?v&2|Ha1OCt z4zl^Y%Vd+E;^k@{I0(aApafu+bbXfuR{{ZG#iGk(li!VFo>V7nD1hVsl3QL*2)`f3 z+S?%(4!cY?`6*7nTWcVj{1ktyX_R`Y0uaMF^m~W6T@>Qo z&&9xcW?9y?$$0$9z|_?99U;aW3KXj-V5_t1D=S+dNkCjVC(X+DpvNk}ow@0*7pkg0 z{A$~;vEvWk8MwenzfY%91&*IU>>q(W5)TaDy*~45GOnJVPKj8lO}&M-^u0x7r=H6H3;V}zqVeKeb05CkA5 zO}+yF+jbBDxBaU3dLs{?Kumhsif`0R;rh6g00000NkvXXu0mjfyRpv7 literal 0 HcmV?d00001 diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/marker--plus.png b/Core/src/org/sleuthkit/autopsy/timeline/images/marker--plus.png new file mode 100644 index 0000000000000000000000000000000000000000..cb98edc78ae1bd4c79064cf09091b9864d2feacf GIT binary patch literal 656 zcmV;B0&o3^P)1RCwBaQ(bG*U=%)Ynyj_j73Red z1#gNvwz;`%?6L}au`2b>o52ti2E%_Kx+`zQKTz;$s9?raCNiPo=&ocoN<-tN@(;1SrznVBGjxoyMCN(IXGy36EK zobvmfx*j|N7C^(&wBPjLaySfvX}U~4#oc_Tt_RN!aNQp&TU+N(#p6Q@!^1!z;{yXw z$z<}BKlBP%uvS*p#E>k1a{lU9I{gzw z5uA1#h{>n;o#kkC>3$UcdxbixHaS;HJ3G5tu~?(oiOK&sA}XIpq!ivZi>0Z53YZ-j zILhkx?d@G)42VCSlUAQxKVm?O%eSxGl!?LO2L+mcf!lpuEEEhIM<9Nl3|;#g6SH7B zuxIZ9F|>IWyGi+%*fkvy;3$A%fp4b+XD%f!%9N}iyT8r57G?)(5>>3ceK8HpB&(v>^pDBvViL?c8 q#(*BFrH4Q8v`j?+oD9Mr0R{lL<06*LsCZ%k0000 { - setCenter(chart); //clear masker pane + setCenter(center); //clear masker pane setCursor(Cursor.DEFAULT); }); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java index db366db707..0f34db262f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java @@ -39,6 +39,10 @@ import org.sleuthkit.autopsy.timeline.actions.Forward; */ public interface TimeLineChart { + + + + // void setController(TimeLineController controller); IntervalSelector getIntervalSelector(); @@ -62,6 +66,7 @@ public interface TimeLineChart { public TimeLineController getController(); + ContextMenu getChartContextMenu(); ContextMenu getChartContextMenu(MouseEvent m); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java index fee0522fa6..6e2cf15534 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java @@ -155,6 +155,8 @@ final class EventCountsChart extends StackedBarChart implements return new CountsIntervalSelector(this); } + + /** * used by {@link CountsViewPane#BarClickHandler} to close the context menu * when the bar menu is requested diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java index 1e011c3245..dd08141bc1 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java @@ -114,7 +114,9 @@ final class DateAxis extends Axis { * by the data. */ DateAxis() { + setTickLabelGap(0); setAutoRanging(false); + setTickLabelsVisible(false); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 1bca199dbb..9ed937e54c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -43,6 +43,7 @@ import javafx.scene.control.RadioButton; import javafx.scene.control.ScrollBar; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.Slider; +import javafx.scene.control.SplitPane; import javafx.scene.control.ToggleGroup; import javafx.scene.control.TreeItem; import javafx.scene.effect.Effect; @@ -66,10 +67,12 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; 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.Event; 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.zooming.DescriptionLoD; /** @@ -84,20 +87,21 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; * EventTypeMap, and dataSets is all linked directly to the ClusterChart which * must only be manipulated on the JavaFx thread. */ -public class DetailViewPane extends AbstractVisualizationPane, EventDetailsChart> { +public class DetailViewPane extends AbstractVisualizationPane, EventDetailsChart> { 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 dateAxis = new DateAxis(); - private final Axis verticalAxis = new EventAxis(); + private final DateAxis detailsChartDateAxis = new DateAxis(); + private final DateAxis pinnedDateAxis = new DateAxis(); + private final Axis verticalAxis = new EventAxis<>(); private final ScrollBar vertScrollBar = new ScrollBar(); private final Region scrollBarSpacer = new Region(); - private MultipleSelectionModel>> treeSelectionModel; - private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private MultipleSelectionModel> treeSelectionModel; + private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); public ObservableList getEventStripes() { return chart.getEventStripes(); @@ -118,25 +122,27 @@ public class DetailViewPane extends AbstractVisualizationPane()); + pinnedChart.setMinSize(100, 100); setChartClickHandler(); //can we push this into chart + SplitPane splitPane = new SplitPane(pinnedChart, chart); + splitPane.setOrientation(Orientation.VERTICAL); chart.setData(dataSeries); - setCenter(chart); + setCenter(splitPane); 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()); + 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()); + scrollBarSpacer.minHeightProperty().bind(detailsChartDateAxis.heightProperty()); //configure scrollbar vertScrollBar.setOrientation(Orientation.VERTICAL); @@ -181,21 +187,17 @@ public class DetailViewPane extends AbstractVisualizationPane chart.setVScroll(vertScrollBar.getValue())); //maintain highlighted effect on correct nodes - highlightedNodes.addListener((ListChangeListener.Change> change) -> { + highlightedNodes.addListener((ListChangeListener.Change> change) -> { while (change.next()) { - change.getAddedSubList().forEach(node -> { - node.applyHighlightEffect(true); - }); - change.getRemoved().forEach(node -> { - node.applyHighlightEffect(false); - }); + change.getAddedSubList().forEach(EventNodeBase::applyHighlightEffect); + change.getRemoved().forEach(EventNodeBase::clearHighlightEffect); } }); selectedNodes.addListener((Observable observable) -> { highlightedNodes.clear(); selectedNodes.stream().forEach((tn) -> { - for (EventBundleNodeBase n : chart.getNodes((EventBundleNodeBase t) -> + for (EventNodeBase n : chart.getNodes((EventNodeBase t) -> t.getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); } @@ -211,14 +213,14 @@ public class DetailViewPane extends AbstractVisualizationPane>> selectionModel) { + public void setSelectionModel(MultipleSelectionModel> selectionModel) { this.treeSelectionModel = selectionModel; treeSelectionModel.getSelectedItems().addListener((Observable observable) -> { highlightedNodes.clear(); - for (TreeItem> tn : treeSelectionModel.getSelectedItems()) { + for (TreeItem tn : treeSelectionModel.getSelectedItems()) { - for (EventBundleNodeBase n : chart.getNodes((EventBundleNodeBase t) -> + for (EventNodeBase n : chart.getNodes((EventNodeBase t) -> t.getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); } @@ -238,17 +240,17 @@ public class DetailViewPane extends AbstractVisualizationPane getXAxis() { - return dateAxis; + return detailsChartDateAxis; } @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,7 +264,7 @@ public class DetailViewPane extends AbstractVisualizationPane c1, Boolean selected) { + protected void applySelectionEffect(EventNodeBase c1, Boolean selected) { c1.applySelectionEffect(selected); } @@ -380,11 +382,11 @@ public class DetailViewPane extends AbstractVisualizationPane { + + static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS + static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS + + public void requestTimelineChartLayout(); + + public Node asNode(); + + public ObservableList getEventStripes(); + + public ObservableList> getSelectedNodes(); + + double layoutEventBundleNodes(final Collection> nodes, final double minY); + + @NbBundle.Messages({"HideDescriptionAction.displayName=Hide", + "HideDescriptionAction.displayMsg=Hide this group from the details view."}) + static class HideDescriptionAction extends Action { + + 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.requestTimelineChartLayout()); + chart.getController().getQuickHideFilters().add(testFilter); + return testFilter; + }); + descriptionFilter.setSelected(true); + }); + } + } + + @NbBundle.Messages({"UnhideDescriptionAction.displayName=Unhide"}) + static class UnhideDescriptionAction extends Action { + + 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)) + ); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java index 43289890ea..518181deed 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java @@ -22,21 +22,21 @@ 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.Event; /** * 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 { +class EventAxis extends Axis { @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 +46,17 @@ class EventAxis extends Axis { } @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 +66,7 @@ class EventAxis extends Axis { } @Override - protected List calculateTickValues(double length, Object range) { + protected List calculateTickValues(double length, Object range) { return Collections.emptyList(); } @@ -76,7 +76,7 @@ class EventAxis extends Axis { } @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. } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index a7e176dc76..4ad9b30a1b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -18,6 +18,7 @@ */ 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; @@ -33,7 +34,6 @@ import javafx.event.EventHandler; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.Button; -import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Border; @@ -49,13 +49,14 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.timeline.datamodel.Event; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; 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 static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; @@ -63,13 +64,12 @@ import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; /** * */ -final public class EventClusterNode extends EventBundleNodeBase { +final public class EventClusterNode extends MultiEventNodeBase { 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; @@ -80,14 +80,31 @@ final public class EventClusterNode extends EventBundleNodeBase { +// TimeLineController controller = getChart().getController(); +// if (controller.getPinnedEventIDs().contains(eventID)) { +// new UnPinEventAction(controller, getEventCluster().getEventIDs()).handle(actionEvent); +// pinButton.setGraphic(new ImageView(PIN)); +// } else { +// new PinEventAction(controller, getEventCluster().getEventIDs()).handle(actionEvent); +// pinButton.setGraphic(new ImageView(UNPIN)); +// } +// }); +// configureActionButton(pinButton); +// } } } - public EventClusterNode(EventDetailsChart chart, EventCluster eventCluster, EventStripeNode parentNode) { + public EventClusterNode(DetailsChart chart, EventCluster eventCluster, EventStripeNode parentNode) { super(chart, eventCluster, parentNode); subNodePane.setBorder(clusterBorder); @@ -98,7 +115,6 @@ final public class EventClusterNode extends EventBundleNodeBase bundles = get(); //clear the existing subnodes - List transform = subNodes.stream().flatMap(new StripeFlattener()).collect(Collectors.toList()); - chart.getEventStripes().removeAll(transform); + List transform = subNodes.stream().flatMap(new StripeFlattener()).collect(Collectors.toList()); + ((EventDetailsChart) getChart()).getEventStripes().removeAll(transform); subNodes.clear(); if (bundles.isEmpty()) { getChildren().setAll(subNodePane, infoHBox); descLOD.set(getEventBundle().getDescriptionLoD()); } else { - chart.getEventStripes().addAll(bundles); + ((EventDetailsChart) getChart()).getEventStripes().addAll(bundles); subNodes.addAll(Lists.transform(bundles, EventClusterNode.this::createChildNode)); getChildren().setAll(new VBox(infoHBox, subNodePane)); descLOD.set(loadedDescriptionLoD); @@ -209,19 +228,25 @@ final public class EventClusterNode extends EventBundleNodeBase createChildNode(EventStripe stripe) { +// return new EventStripeNode(getChart(), stripe, this); + + if (stripe.getEventIDs().size() == 1) { + return new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(stripe.getEventIDs())), this); + } else { + return new EventStripeNode(getChart(), stripe, this); + } } EventCluster getEventCluster() { @@ -230,8 +255,8 @@ final public class EventClusterNode extends EventBundleNodeBase implements TimeLineChart { +public final class EventDetailsChart extends XYChart implements DetailsChart { 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; @@ -145,7 +144,7 @@ public final class EventDetailsChart extends XYChart impl */ private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); - final ObservableList> selectedNodes; + final ObservableList> selectedNodes; /** * the group that all event nodes are added to. This facilitates scrolling * by allowing a single translation of this group. @@ -154,8 +153,8 @@ public final class EventDetailsChart extends XYChart impl @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private final ObservableList eventStripes = FXCollections.observableArrayList(); - private final ObservableList< EventStripeNode> stripeNodes = FXCollections.observableArrayList(); - private final ObservableList< EventStripeNode> sortedStripeNodes = stripeNodes.sorted(Comparator.comparing(EventStripeNode::getStartMillis)); + private final ObservableList< EventNodeBase> stripeNodes = FXCollections.observableArrayList(); + private final ObservableList< EventNodeBase> sortedStripeNodes = stripeNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)); private final Map projectionMap = new ConcurrentHashMap<>(); /** @@ -190,7 +189,7 @@ public final class EventDetailsChart extends XYChart impl */ final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); - EventDetailsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { + EventDetailsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { super(dateAxis, verticalAxis); this.controller = controller; @@ -249,7 +248,21 @@ public final class EventDetailsChart extends XYChart impl this.selectedNodes.addListener(new SelectionChangeHandler()); } - ObservableList getEventStripes() { + @Override + public void requestTimelineChartLayout() { + requestChartLayout(); + } + + public ObservableList> getSelectedNodes() { + return selectedNodes; + } + + @Override + public Node asNode() { + return this; + } + + public ObservableList getEventStripes() { return eventStripes; } @@ -390,14 +403,17 @@ public final class EventDetailsChart extends XYChart impl */ void addDataItem(Data data) { final EventStripe eventStripe = data.getYValue(); - - EventStripeNode stripeNode = new EventStripeNode(EventDetailsChart.this, eventStripe, null); - + EventNodeBase newNode; + if (eventStripe.getEventIDs().size() == 1) { + newNode = new SingleEventNode(this, controller.getEventsModel().getEventById(Iterables.getOnlyElement(eventStripe.getEventIDs())), null); + } else { + newNode = new EventStripeNode(EventDetailsChart.this, eventStripe, null); + } Platform.runLater(() -> { eventStripes.add(eventStripe); - stripeNodes.add(stripeNode); - nodeGroup.getChildren().add(stripeNode); - data.setNode(stripeNode); + stripeNodes.add(newNode); + nodeGroup.getChildren().add(newNode); + data.setNode(newNode); }); } @@ -410,7 +426,7 @@ public final class EventDetailsChart extends XYChart impl */ void removeDataItem(Data data) { Platform.runLater(() -> { - EventStripeNode removedNode = (EventStripeNode) data.getNode(); + EventNodeBase removedNode = (EventNodeBase) data.getNode(); eventStripes.removeAll(new StripeFlattener().apply(removedNode).collect(Collectors.toList())); stripeNodes.removeAll(removedNode); nodeGroup.getChildren().removeAll(removedNode); @@ -434,10 +450,10 @@ public final class EventDetailsChart extends XYChart impl if (bandByType.get()) { sortedStripeNodes.stream() - .collect(Collectors.groupingBy(EventStripeNode::getEventType)).values() + .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); } else { - maxY.set(layoutEventBundleNodes(sortedStripeNodes.sorted(Comparator.comparing(EventStripeNode::getStartMillis)), 0)); + maxY.set(layoutEventBundleNodes(sortedStripeNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); } layoutProjectionMap(); setCursor(null); @@ -450,12 +466,12 @@ public final class EventDetailsChart extends XYChart impl /** * @return all the nodes that pass the given predicate */ - synchronized Iterable> getNodes(Predicate> p) { + synchronized Iterable> getNodes(Predicate> p) { //use this recursive function to flatten the tree of nodes into an single stream. - Function, Stream>> stripeFlattener = - new Function, Stream>>() { + Function, Stream>> stripeFlattener = + new Function, Stream>>() { @Override - public Stream> apply(EventBundleNodeBase node) { + public Stream> apply(EventNodeBase node) { return Stream.concat( Stream.of(node), node.getSubNodes().stream().flatMap(this::apply)); @@ -506,7 +522,7 @@ public final class EventDetailsChart extends XYChart impl * * @return the maximum y coordinate used by any of the layed out nodes. */ - double layoutEventBundleNodes(final Collection> nodes, final double minY) { + public double layoutEventBundleNodes(final Collection> nodes, final double minY) { // map from y-ranges to maximum x TreeRangeMap maxXatY = TreeRangeMap.create(); @@ -514,7 +530,7 @@ public final class EventDetailsChart extends XYChart impl 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) { + for (EventNodeBase bundleNode : nodes) { //is the node hiden by a quick hide filter? boolean quickHide = activeQuickHidefilters.contains(bundleNode.getDescription()); if (quickHide) { @@ -594,7 +610,7 @@ public final class EventDetailsChart extends XYChart impl * @param bundleNode the Node to layout * @param descriptionWdith the maximum width for the description text */ - private void layoutBundleHelper(final EventBundleNodeBase bundleNode) { + private void layoutBundleHelper(final EventNodeBase< ?> bundleNode) { //make sure it is shown bundleNode.setVisible(true); bundleNode.setManaged(true); @@ -686,7 +702,7 @@ public final class EventDetailsChart extends XYChart impl } } - private class SelectionChangeHandler implements ListChangeListener> { + private class SelectionChangeHandler implements ListChangeListener> { private final Axis dateAxis; @@ -695,18 +711,18 @@ public final class EventDetailsChart extends XYChart impl } @Override - public void onChanged(ListChangeListener.Change> change) { + public void onChanged(ListChangeListener.Change> change) { while (change.next()) { - change.getRemoved().forEach((EventBundleNodeBase removedNode) -> { - removedNode.getEventBundle().getClusters().forEach(cluster -> { + change.getRemoved().forEach((EventNodeBase removedNode) -> { + removedNode.getEvent().getClusters().forEach(cluster -> { Line removedLine = projectionMap.remove(cluster); getChartChildren().removeAll(removedLine); }); }); - change.getAddedSubList().forEach((EventBundleNodeBase addedNode) -> { + change.getAddedSubList().forEach((EventNodeBase addedNode) -> { - for (EventCluster range : addedNode.getEventBundle().getClusters()) { + for (EventCluster range : addedNode.getEvent().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 @@ -725,44 +741,4 @@ public final class EventDetailsChart extends XYChart impl } } - @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)) - ); - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java new file mode 100644 index 0000000000..a047fb3908 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -0,0 +1,258 @@ +/* + * 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.detailview; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.EventHandler; +import javafx.scene.Node; +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.StackPane; +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.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.Event; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; +import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; + +/** + * + */ +public abstract class EventNodeBase extends StackPane { + + static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N NON-NLS + static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N + static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N + static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N + static final Image PIN = new Image("/org/sleuthkit/autopsy/timeline/images/marker--plus.png"); // NON-NLS //NOI18N + static final Image UNPIN = new Image("/org/sleuthkit/autopsy/timeline/images/marker--minus.png"); // NON-NLS //NOI18N + + static final Map dropShadowMap = new ConcurrentHashMap<>(); + + private final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); + + final Type ievent; + + final EventNodeBase parentNode; + final Label descrLabel = new Label(); + final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + final SimpleObjectProperty descVisibility = new SimpleObjectProperty<>(); + + static void configureActionButton(ButtonBase 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); + } + private Timeline timeline; + final DetailsChart chart; + + public EventNodeBase(Type ievent, EventNodeBase parent, DetailsChart chart) { + this.chart = chart; + this.ievent = ievent; + this.parentNode = parent; + + descVisibility.addListener(observable -> setDescriptionVisibiltiyImpl(descVisibility.get())); +// descVisibility.set(DescriptionVisibility.SHOWN); //trigger listener for initial value + + //set up mouse hover effect and tooltip + setOnMouseEntered(mouseEntered -> { + + Tooltip.uninstall(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); + showHoverControls(true); + toFront(); + }); + setOnMouseExited(mouseExited -> { + showHoverControls(false); + if (parentNode != null) { + parentNode.showHoverControls(true); + } else { + Tooltip.install(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); + } + }); + setOnMouseClicked(new ClickHandler()); + } + + public Type getEvent() { + return ievent; + } + + protected void layoutChildren() { + super.layoutChildren(); + } + + abstract public TimeLineChart getChart(); + + 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); + } + } + + abstract void installTooltip(); + + void enableTooltip(boolean toolTipEnabled) { + if (toolTipEnabled) { + Tooltip.install(this, tooltip); + } else { + Tooltip.uninstall(this, tooltip); + } + } + + EventType getEventType() { + return ievent.getEventType(); + } + + long getStartMillis() { + return ievent.getStartMillis(); + } + + final double getLayoutXCompensation() { + return parentNode != null + ? getChart().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(); + } + + abstract void requestChartLayout(); + + void setDescriptionVisibility(DescriptionVisibility get) { + descVisibility.set(get); + } + + abstract void setDescriptionVisibiltiyImpl(DescriptionVisibility get); + + abstract void setMaxDescriptionWidth(double descriptionWidth); + + abstract public List> getSubNodes(); + + abstract void applyHighlightEffect(boolean b); + + void applyHighlightEffect() { + applyHighlightEffect(true); + } + + void clearHighlightEffect() { + applyHighlightEffect(false); + } + + abstract Collection getEventIDs(); + + void applySelectionEffect(Boolean selected) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + static class PinEventAction extends Action { + + @NbBundle.Messages({"PinEventAction.text=Pin"}) + PinEventAction(TimeLineController controller, Set eventIds) { + super(Bundle.PinEventAction_text()); + setEventHandler(actionEvent -> controller.pinEvents(eventIds)); + setGraphic(new ImageView(PIN)); + } + } + + static class UnPinEventAction extends Action { + + @NbBundle.Messages({"UnPinEventAction.text=Unpin"}) + UnPinEventAction(TimeLineController controller, Set eventIds) { + super(Bundle.UnPinEventAction_text()); + setEventHandler(actionEvent -> controller.unPinEvents(eventIds)); + setGraphic(new ImageView(UNPIN)); + } + } + + /** + * event handler used for mouse events on {@link EventNodeBase}s + */ + class ClickHandler implements EventHandler { + + 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.getSelectedNodes().add(EventNodeBase.this); + } else if (t.isShortcutDown()) { + chart.getSelectedNodes().removeAll(EventNodeBase.this); + } else { + chart.getSelectedNodes().setAll(EventNodeBase.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(EventNodeBase.this, t.getScreenX(), t.getScreenY()); + t.consume(); + } + } + + } + + abstract EventHandler getDoubleClickHandler(); + + abstract Collection getActions(); + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index fb8c4a68b6..98822c0f25 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -19,6 +19,7 @@ */ 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; @@ -34,12 +35,13 @@ 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 { +final public class EventStripeNode extends MultiEventNodeBase { private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName()); private Button hideButton; @@ -56,17 +58,16 @@ final public class EventStripeNode extends EventBundleNodeBase createChildNode(EventCluster cluster) { + if (cluster.getEventIDs().size() == 1) { + return new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); + } else { + return new EventClusterNode(getChart(), cluster, this); + } } @Override @@ -155,6 +160,6 @@ final public class EventStripeNode extends EventBundleNodeBase getActions() { - return Arrays.asList(chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD())); + return Arrays.asList(new HideDescriptionAction(getDescription(), ievent.getDescriptionLoD(), chart)); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java similarity index 67% rename from Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java rename to Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java index 5d5eb5aa07..d6db0de0ce 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java @@ -23,7 +23,6 @@ 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; @@ -32,7 +31,6 @@ 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; @@ -40,17 +38,9 @@ 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; @@ -61,22 +51,20 @@ 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.MultiEvent; 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.EventType; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; @@ -85,36 +73,17 @@ import org.sleuthkit.datamodel.TskCoreException; * */ @NbBundle.Messages({"EventBundleNodeBase.toolTip.loading=loading..."}) -public abstract class EventBundleNodeBase, ParentType extends EventBundle, ParentNodeType extends EventBundleNodeBase> extends StackPane { +public abstract class MultiEventNodeBase< BundleType extends MultiEvent, ParentType extends MultiEvent, ParentNodeType extends MultiEventNodeBase< + ParentType, BundleType, ?>> extends EventNodeBase { - 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 + private static final Logger LOGGER = Logger.getLogger(MultiEventNodeBase.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 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 descLOD = new SimpleObjectProperty<>(); - final SimpleObjectProperty descVisibility = new SimpleObjectProperty<>(); - protected final BundleType eventBundle; - - protected final ParentNodeType parentNode; final SleuthkitCase sleuthkitCase; final FilteredEventsModel eventsModel; @@ -123,9 +92,9 @@ public abstract class EventBundleNodeBase subNodes = FXCollections.observableArrayList(); + final ObservableList> 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); @@ -135,10 +104,8 @@ public abstract class EventBundleNodeBase chart.requestChartLayout()); + heightProperty().addListener(heightProp -> chart.requestTimelineChartLayout()); Platform.runLater(() -> setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()) ); @@ -178,7 +145,7 @@ public abstract class EventBundleNodeBase { - Tooltip.uninstall(chart, AbstractVisualizationPane.getDefaultTooltip()); + Tooltip.uninstall(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); showHoverControls(true); toFront(); }); @@ -187,28 +154,29 @@ public abstract class EventBundleNodeBase setDescriptionVisibiltiyImpl(descVisibility.get())); - descVisibility.set(DescriptionVisibility.SHOWN); //trigger listener for initial value Bindings.bindContent(subNodePane.getChildren(), subNodes); } + @Override + void requestChartLayout() { + getChart().requestTimelineChartLayout(); + } + + public DetailsChart getChart() { + return chart; + } + 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; + return ievent; } /** @@ -234,7 +202,7 @@ public abstract class EventBundleNodeBase tooltTipTask = new Task() { { @@ -244,10 +212,10 @@ public abstract class EventBundleNodeBase hashSetCounts = new HashMap<>(); - if (eventBundle.getEventIDsWithHashHits().isEmpty() == false) { + if (ievent.getEventIDsWithHashHits().isEmpty() == false) { try { //TODO:push this to DB - for (TimeLineEvent tle : eventsModel.getEventsById(eventBundle.getEventIDsWithHashHits())) { + for (SingleEvent tle : eventsModel.getEventsById(ievent.getEventIDsWithHashHits())) { Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); for (String hashSetName : hashSetNames) { hashSetCounts.merge(hashSetName, 1L, Long::sum); @@ -262,8 +230,8 @@ public abstract class EventBundleNodeBase tagCounts = new HashMap<>(); - if (eventBundle.getEventIDsWithTags().isEmpty() == false) { - tagCounts.putAll(eventsModel.getTagCountsByTagName(eventBundle.getEventIDsWithTags())); + if (ievent.getEventIDsWithTags().isEmpty() == false) { + tagCounts.putAll(eventsModel.getTagCountsByTagName(ievent.getEventIDsWithTags())); } String tagCountsString = tagCounts.entrySet().stream() .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) @@ -309,23 +277,10 @@ public abstract class EventBundleNodeBase getSubNodes() { + public List> 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(); } @@ -357,35 +312,23 @@ public abstract class EventBundleNodeBase 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); + Platform.runLater(chart::requestTimelineChartLayout); } timeline = new Timeline(new KeyFrame(Duration.millis(100), new KeyValue(layoutXProperty(), xLeft), new KeyValue(layoutYProperty(), yTop)) ); - timeline.setOnFinished(finished -> Platform.runLater(chart::requestChartLayout)); + timeline.setOnFinished(finished -> Platform.runLater(chart::requestTimelineChartLayout)); timeline.play(); } @@ -393,41 +336,4 @@ public abstract class EventBundleNodeBase getActions(); - /** - * event handler used for mouse events on {@link EventStripeNode}s - */ - private class ClickHandler implements EventHandler { - - 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(); - } - } - } - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java new file mode 100644 index 0000000000..c567985fa6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java @@ -0,0 +1,380 @@ +/* + * 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.detailview; + +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.Map; +import java.util.MissingResourceException; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.SetChangeListener; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.Group; +import javafx.scene.Node; +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.controlsfx.control.action.ActionUtils; +import org.joda.time.DateTime; +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.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; +import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; +import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; +import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; + +/** + * + */ +public final class PinnedEventsChart extends XYChart implements DetailsChart { + + @Override + public ContextMenu getChartContextMenu() { + return chartContextMenu; + } + private ContextMenu chartContextMenu; + + @Override + public ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException { + if (chartContextMenu != null) { + chartContextMenu.hide(); + } + + chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(//new EventDetailsChart.PlaceMarkerAction(clickEvent), + TimeLineChart.newZoomHistoyActionGroup(controller))); + chartContextMenu.setAutoHide(true); + return chartContextMenu; + } +// final ObservableList> selectedNodes; + + private static final String styleSheet = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS + + private final TimeLineController controller; + private final FilteredEventsModel filteredEvents; + /** + * 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 events = FXCollections.observableArrayList(); + private final ObservableList< SingleEventNode> eventNodes = FXCollections.observableArrayList(); + private final ObservableList< SingleEventNode> sortedEventNodes = eventNodes.sorted(Comparator.comparing(SingleEventNode::getStartMillis)); + private double descriptionWidth; + + Map eventMap = new HashMap<>(); + + /** + * + * @param controller the value of controller + * @param dateAxis the value of dateAxis + * @param verticalAxis the value of verticalAxis + * @param selectedNodes1 the value of selectedNodes1 + */ + PinnedEventsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis) { + 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 -> { + }); + + 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); + final Series series = new Series<>(); + setData(FXCollections.observableArrayList()); + getData().add(series); +// //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 chartDragHandler = new ChartDragHandler<>(this); +// setOnMousePressed(chartDragHandler); +// setOnMouseReleased(chartDragHandler); +// setOnMouseDragged(chartDragHandler); +// +// setOnMouseClicked(new MouseClickedHandler<>(this)); + controller.getPinnedEventIDs().addListener((SetChangeListener.Change change) -> { + if (change.wasAdded()) { + SingleEvent eventById = controller.getEventsModel().getEventById(change.getElementAdded()); + Data data1 = new Data<>(new DateTime(eventById.getStartMillis()), eventById); + series.getData().add(data1); + addDataItem(data1); + } + if (change.wasRemoved()) { + final SingleEvent eventById = controller.getEventsModel().getEventById(change.getElementRemoved()); + Data data1 = new Data<>(new DateTime(eventById.getStartMillis()), eventById); + series.getData().removeIf(t -> eventById.equals(t.getYValue())); + removeDataItem(data1); + } + + requestChartLayout(); + }); + +// this.selectedNodes = selectedNodes; +// selectedNodes.addListener(new SelectionChangeHandler()); + } + + @Override + public Node asNode() { + return this; + } + + @Override + public ObservableList getEventStripes() { + return FXCollections.emptyObservableList(); + } + + @Override + public IntervalSelector getIntervalSelector() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void setIntervalSelector(IntervalSelector newIntervalSelector) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public IntervalSelector newIntervalSelector() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void clearIntervalSelector() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public TimeLineController getController() { + return controller; + } + + + @Override + public void requestTimelineChartLayout() { + requestChartLayout(); + } + + @Override + public ObservableList> getSelectedNodes() { + return FXCollections.observableArrayList(); + } + + @Override + protected void dataItemAdded(Series series, int itemIndex, Data item) { + } + + @Override + protected void dataItemRemoved(Data item, Series series) { + } + + @Override + protected void dataItemChanged(Data item) { + } + + @Override + protected void seriesAdded(Series series, int seriesIndex) { + } + + @Override + protected void seriesRemoved(Series series) { + } + /** + * the maximum y value used so far during the most recent layout pass + */ + private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); + + @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(sortedEventNodes.sorted(Comparator.comparing(SingleEventNode::getStartMillis)), 0)); +// } + setCursor(null); + } + private static final int MINIMUM_EVENT_NODE_GAP = 4; + private final static int MINIMUM_ROW_HEIGHT = 24; + + private double getXForEpochMillis(Long millis) { + DateTime dateTime = new DateTime(millis); + return getXAxis().getDisplayPosition(dateTime); + } + + @Override + public double layoutEventBundleNodes(final Collection> nodes, final double minY) { + // map from y-ranges to maximum x + TreeRangeMap 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 eventNode : nodes) { + //is the node hiden by a quick hide filter? + + layoutBundleHelper(eventNode); + //get computed height and width + double h = eventNode.getBoundsInLocal().getHeight(); + double w = eventNode.getBoundsInLocal().getWidth(); + //get left and right x coords from axis plus computed width + double xLeft = getXForEpochMillis(eventNode.getStartMillis()) - eventNode.getLayoutXCompensation(); + double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP; + + //initial test position + double yTop = computeYTop(minY, h, maxXatY, xLeft, xRight); + + localMax = Math.max(yTop + h, localMax); + + if ((xLeft != eventNode.getLayoutX()) || (yTop != eventNode.getLayoutY())) { + //animate node to new position + eventNode.animateTo(xLeft, yTop); + } + + } + return localMax; //return new max + } + + /** + * expose as protected + */ + @Override + protected void requestChartLayout() { + super.requestChartLayout(); + } + + private double computeYTop(double yMin, double h, TreeRangeMap 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; + } + + private void layoutBundleHelper(final EventNodeBase eventNode) { + //make sure it is shown + eventNode.setVisible(true); + eventNode.setManaged(true); + //apply advanced layout description visibility options +// eventNode.setDescriptionVisibility(descrVisibility.get()); +// eventNode.setMaxDescriptionWidth(descriptionWidth); + + //do recursive layout + eventNode.layoutChildren(); + } + + /** + * add a dataitem to this chart + * + * @see note in main section of class JavaDoc + * + * @param data + */ + void addDataItem(Data data) { + final SingleEvent event = data.getYValue(); + + SingleEventNode eventNode = new SingleEventNode(PinnedEventsChart.this, event, null); + eventMap.put(event.getEventID(), eventNode); + Platform.runLater(() -> { + events.add(event); + eventNodes.add(eventNode); + nodeGroup.getChildren().add(eventNode); + data.setNode(eventNode); + + }); + } + + /** + * remove a data item from this chart + * + * @see note in main section of class JavaDoc + * + * @param data + */ + void removeDataItem(Data data) { + SingleEventNode removedNode = eventMap.remove(data.getYValue().getEventID()); + Platform.runLater(() -> { + events.removeAll(data.getYValue()); + eventNodes.removeAll(removedNode); + nodeGroup.getChildren().removeAll(removedNode); + data.setNode(null); + }); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java new file mode 100644 index 0000000000..57219054a9 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java @@ -0,0 +1,202 @@ +/* + * 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.detailview; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.OverrunStyle; +import javafx.scene.image.ImageView; +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 static javafx.scene.layout.Region.USE_PREF_SIZE; +import javafx.scene.paint.Color; +import org.apache.commons.lang3.StringUtils; +import org.controlsfx.control.action.Action; +import org.joda.time.DateTime; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; +import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configureActionButton; +import static org.sleuthkit.autopsy.timeline.ui.detailview.MultiEventNodeBase.CORNER_RADII_3; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; + +/** + * + */ +final class SingleEventNode extends EventNodeBase { + + private final DetailsChart chart; + final Background defaultBackground; + private final Color evtColor; + + final ImageView hashIV = new ImageView(HASH_PIN); + final ImageView tagIV = new ImageView(TAG); + final HBox infoHBox = new HBox(5, descrLabel, hashIV, tagIV); + + 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); + private final ImageView eventTypeImageView = new ImageView(); + private Button pinButton; + + @Override + EventHandler getDoubleClickHandler() { + return mouseEvent -> { + }; + } + + @Override + Collection getActions() { + TimeLineController controller = getChart().getController(); + if (controller.getPinnedEventIDs().contains(ievent.getEventID())) { + return Arrays.asList(new UnPinEventAction(controller, ievent.getEventIDs())); + } else { + return Arrays.asList(new PinEventAction(controller, ievent.getEventIDs())); + } + } + + SingleEventNode(DetailsChart chart, SingleEvent event, MultiEventNodeBase parent) { + super(event, parent, chart); + this.chart = 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); + if (event.getEventIDsWithHashHits().isEmpty()) { + show(hashIV, false); + } + if (event.getEventIDsWithTags().isEmpty()) { + show(tagIV, false); + } + + if (chart.getController().getEventsModel().getEventTypeZoom() == EventTypeZoomLevel.SUB_TYPE) { + evtColor = getEventType().getColor(); + } else { + evtColor = getEventType().getBaseType().getColor(); + } + final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); + setBorder(clusterBorder); + defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY)); + setBackground(defaultBackground); + setMaxWidth(USE_PREF_SIZE); + infoHBox.setMaxWidth(USE_PREF_SIZE); + getChildren().add(infoHBox); + + } + + @Override + public TimeLineChart getChart() { + return chart; + } + + @Override + public List> getSubNodes() { + return Collections.emptyList(); + } + + void installActionButtons() { + if (pinButton == null) { + pinButton = new Button("", new ImageView(PIN)); + infoHBox.getChildren().add(pinButton); + configureActionButton(pinButton); + + pinButton.setOnAction(actionEvent -> { + TimeLineController controller = getChart().getController(); + if (controller.getPinnedEventIDs().contains(ievent.getEventID())) { + new UnPinEventAction(controller, ievent.getEventIDs()).handle(actionEvent); + pinButton.setGraphic(new ImageView(PIN)); + } else { + new PinEventAction(controller, ievent.getEventIDs()).handle(actionEvent); + pinButton.setGraphic(new ImageView(UNPIN)); + } + }); + + } + } + + @Override + void showHoverControls(final boolean showControls) { + super.showHoverControls(showControls); + installActionButtons(); + show(pinButton, showControls); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); //To change body of generated methods, choose Tools | Templates. + } + + @Override + void installTooltip() { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + String getDescription() { + return ievent.getFullDescription(); + } + + @Override + void requestChartLayout() { + chart.requestTimelineChartLayout(); + } + + @Override + void applyHighlightEffect(boolean b) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + void setMaxDescriptionWidth(double descriptionWidth) { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + void setDescriptionVisibiltiyImpl(DescriptionVisibility descrVis) { + + switch (descrVis) { + case HIDDEN: + descrLabel.setText(""); + break; + case COUNT_ONLY: + descrLabel.setText(""); + break; + default: + case SHOWN: + String description = ievent.getFullDescription(); + description = parentNode != null + ? " ..." + StringUtils.substringAfter(description, parentNode.getDescription()) + : description; + descrLabel.setText(description); + break; + } + } + + @Override + Collection getEventIDs() { + return getEvent().getEventIDs(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java index 391c498ef4..4baa792fa8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java @@ -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.Event; /** * 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> { +class StripeFlattener implements Function, Stream> { @Override - public Stream apply(EventStripeNode node) { + public Stream apply(EventNodeBase node) { return Stream.concat( - Stream.of(node.getEventStripe()), + Stream.of(node.getEvent()), node.getSubNodes().stream().flatMap(clusterNode -> clusterNode.getSubNodes().stream().flatMap(this))); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java index d85f9b564f..57a8378ca2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -25,7 +25,8 @@ 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.MultiEvent; +import org.sleuthkit.autopsy.timeline.datamodel.Event; /** * @@ -36,14 +37,14 @@ class EventDescriptionTreeItem extends NavTreeItem { * maps a description to the child item of this item with that description */ private final Map childMap = new HashMap<>(); - private final EventBundle bundle; - private Comparator>> comparator = TreeComparator.Description; + private final Event bundle; + private Comparator> comparator = TreeComparator.Description; - public EventBundle getEventBundle() { + public Event getEvent() { return bundle; } - EventDescriptionTreeItem(EventBundle g, Comparator>> comp) { + EventDescriptionTreeItem(Event g, Comparator> comp) { bundle = g; comparator = comp; setValue(g); @@ -55,8 +56,8 @@ class EventDescriptionTreeItem extends NavTreeItem { } @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - public void insert(Deque> path) { - EventBundle head = path.removeFirst(); + public void insert(Deque> path) { + MultiEvent head = path.removeFirst(); EventDescriptionTreeItem treeItem = childMap.computeIfAbsent(head.getDescription(), description -> { EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(head, comparator); newTreeItem.setExpanded(true); @@ -71,8 +72,8 @@ class EventDescriptionTreeItem extends NavTreeItem { } } - void remove(Deque> path) { - EventBundle head = path.removeFirst(); + void remove(Deque> path) { + MultiEvent head = path.removeFirst(); EventDescriptionTreeItem descTreeItem = childMap.get(head.getDescription()); if (path.isEmpty() == false) { descTreeItem.remove(path); @@ -84,7 +85,7 @@ class EventDescriptionTreeItem extends NavTreeItem { } @Override - void resort(Comparator>> comp, Boolean recursive) { + void resort(Comparator> comp, Boolean recursive) { this.comparator = comp; FXCollections.sort(getChildren(), comp); if (recursive) { @@ -93,7 +94,7 @@ class EventDescriptionTreeItem extends NavTreeItem { } @Override - public NavTreeItem findTreeItemForEvent(EventBundle t) { + public NavTreeItem findTreeItemForEvent(Event t) { if (getValue().getEventType() == t.getEventType() && getValue().getDescription().equals(t.getDescription())) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java index 7c73a4b49f..0021732a28 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -25,7 +25,8 @@ 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.MultiEvent; +import org.sleuthkit.autopsy.timeline.datamodel.Event; class EventTypeTreeItem extends NavTreeItem { @@ -34,9 +35,9 @@ class EventTypeTreeItem extends NavTreeItem { */ private final Map childMap = new HashMap<>(); - private Comparator>> comparator = TreeComparator.Description; + private Comparator> comparator = TreeComparator.Description; - EventTypeTreeItem(EventBundle g, Comparator>> comp) { + EventTypeTreeItem(MultiEvent g, Comparator> comp) { setValue(g); comparator = comp; } @@ -47,8 +48,8 @@ class EventTypeTreeItem extends NavTreeItem { } @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - public void insert(Deque> path) { - EventBundle head = path.removeFirst(); + public void insert(Deque> path) { + MultiEvent head = path.removeFirst(); EventDescriptionTreeItem treeItem = childMap.computeIfAbsent(head.getDescription(), description -> { EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(head, comparator); newTreeItem.setExpanded(true); @@ -63,8 +64,8 @@ class EventTypeTreeItem extends NavTreeItem { } } - void remove(Deque> path) { - EventBundle head = path.removeFirst(); + void remove(Deque> path) { + MultiEvent head = path.removeFirst(); EventDescriptionTreeItem descTreeItem = childMap.get(head.getDescription()); if (descTreeItem != null) { if (path.isEmpty() == false) { @@ -78,7 +79,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public NavTreeItem findTreeItemForEvent(EventBundle t) { + public NavTreeItem findTreeItemForEvent(Event t) { if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) { for (EventDescriptionTreeItem child : childMap.values()) { @@ -92,7 +93,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - void resort(Comparator>> comp, Boolean recursive) { + void resort(Comparator> comp, Boolean recursive) { this.comparator = comp; FXCollections.sort(getChildren(), comp); if (recursive) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java index e06b68e329..a70cfee48a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java @@ -48,7 +48,8 @@ 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.datamodel.MultiEvent; +import org.sleuthkit.autopsy.timeline.datamodel.Event; import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; @@ -66,7 +67,7 @@ final public class EventsTree extends BorderPane { private DetailViewPane detailViewPane; @FXML - private TreeView> eventsTree; + private TreeView eventsTree; @FXML private Label eventsTreeLabel; @@ -84,13 +85,13 @@ final public class EventsTree extends BorderPane { this.detailViewPane = detailViewPane; detailViewPane.setSelectionModel(eventsTree.getSelectionModel()); - detailViewPane.getEventStripes().addListener((ListChangeListener.Change> c) -> { + detailViewPane.getEventStripes().addListener((ListChangeListener.Change> c) -> { //on jfx thread while (c.next()) { - for (EventBundle bundle : c.getAddedSubList()) { + for (MultiEvent bundle : c.getAddedSubList()) { getRoot().insert(bundle); } - for (EventBundle bundle : c.getRemoved()) { + for (MultiEvent bundle : c.getRemoved()) { getRoot().remove(bundle); } } @@ -101,7 +102,7 @@ final public class EventsTree extends BorderPane { detailViewPane.getSelectedNodes().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); detailViewPane.getSelectedNodes().forEach(eventBundleNode -> { - eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventBundleNode.getEventBundle())); + eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventBundleNode.getEvent())); }); }); @@ -114,7 +115,7 @@ 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()) { + for (MultiEvent bundle : detailViewPane.getEventStripes()) { root.insert(bundle); } eventsTree.setRoot(root); @@ -142,10 +143,10 @@ final public class EventsTree extends BorderPane { } /** - * A tree cell to display {@link EventBundle}s. Shows the description, and + * A tree cell to display {@link MultiEvent}s. Shows the description, and * count, as well a a "legend icon" for the event type. */ - private class EventBundleTreeCell extends TreeCell> { + private class EventBundleTreeCell extends TreeCell { private static final double HIDDEN_MULTIPLIER = .6; private final Rectangle rect = new Rectangle(24, 24); @@ -160,7 +161,7 @@ final public class EventsTree extends BorderPane { } @Override - protected void updateItem(EventBundle item, boolean empty) { + protected void updateItem(Event item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { setText(null); @@ -185,7 +186,7 @@ final public class EventsTree extends BorderPane { } else { setDisable(false); text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS - TreeItem> parent = getTreeItem().getParent(); + TreeItem parent = getTreeItem().getParent(); if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) { text = StringUtils.substringAfter(text, parent.getValue().getDescription()); } @@ -213,7 +214,7 @@ final public class EventsTree extends BorderPane { } } - private void registerListeners(Collection filters, EventBundle item) { + private void registerListeners(Collection filters, Event item) { for (DescriptionFilter filter : filters) { if (filter.getDescription().equals(item.getDescription())) { filter.activeProperty().addListener(filterStateChangeListener); @@ -229,8 +230,8 @@ final public class EventsTree extends BorderPane { } } - private void updateHiddenState(EventBundle item) { - TreeItem> treeItem = getTreeItem(); + private void updateHiddenState(Event item) { + TreeItem treeItem = getTreeItem(); hidden.set(controller.getQuickHideFilters().stream(). filter(AbstractFilter::isActive) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java index d2d69248b4..bda411df23 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java @@ -20,7 +20,7 @@ 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.Event; /** * A node in the nav tree. Manages inserts and resorts. Has parents and @@ -28,12 +28,12 @@ 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> { +abstract class NavTreeItem extends TreeItem { abstract long getCount(); - abstract void resort(Comparator>> comp, Boolean recursive); + abstract void resort(Comparator> comp, Boolean recursive); - abstract NavTreeItem findTreeItemForEvent(EventBundle t); + abstract NavTreeItem findTreeItemForEvent(Event t); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java index ccfa97e2dd..644c9d0e0f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -26,7 +26,8 @@ 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.MultiEvent; +import org.sleuthkit.autopsy.timeline.datamodel.Event; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; /** @@ -42,10 +43,10 @@ class RootItem extends NavTreeItem { /** * the comparator if any used to sort the children of this item */ - private Comparator>> comparator = TreeComparator.Type.reversed(); + private Comparator> comparator = TreeComparator.Type.reversed(); - RootItem(Comparator>> comp) { - comp = comp; + RootItem(Comparator> comp) { + this.comparator = comp; } @Override @@ -59,7 +60,7 @@ class RootItem extends NavTreeItem { * @param bundle bundle to add */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - public void insert(EventBundle bundle) { + public void insert(MultiEvent bundle) { EventTypeTreeItem treeItem = childMap.computeIfAbsent(bundle.getEventType().getBaseType(), baseType -> { @@ -72,7 +73,7 @@ class RootItem extends NavTreeItem { } - void remove(EventBundle bundle) { + void remove(MultiEvent bundle) { EventTypeTreeItem typeTreeItem = childMap.get(bundle.getEventType().getBaseType()); if (typeTreeItem != null) { typeTreeItem.remove(getTreePath(bundle)); @@ -84,12 +85,12 @@ class RootItem extends NavTreeItem { } } - static Deque< EventBundle> getTreePath(EventBundle g) { - Deque> path = new ArrayDeque<>(); - Optional> p = Optional.of(g); + static Deque< MultiEvent> getTreePath(MultiEvent g) { + Deque> path = new ArrayDeque<>(); + Optional> p = Optional.of(g); while (p.isPresent()) { - EventBundle parent = p.get(); + MultiEvent parent = p.get(); path.addFirst(parent); p = parent.getParentBundle(); } @@ -98,13 +99,13 @@ class RootItem extends NavTreeItem { } @Override - void resort(Comparator>> comp, Boolean recursive) { + void resort(Comparator> comp, Boolean recursive) { comparator = comp; childMap.values().forEach(ti -> ti.resort(comp, true)); } @Override - public NavTreeItem findTreeItemForEvent(EventBundle t) { + public NavTreeItem findTreeItemForEvent(Event t) { for (EventTypeTreeItem child : childMap.values()) { final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); if (findTreeItemForEvent != null) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java index 31fe8cf43a..317f723cb9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java @@ -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.Event; 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>> { +enum TreeComparator implements Comparator> { Description(Bundle.TreeComparator_Description_displayName()) { @Override - public int compare(TreeItem> o1, TreeItem> o2) { + public int compare(TreeItem o1, TreeItem o2) { return o1.getValue().getDescription().compareTo(o2.getValue().getDescription()); } }, Count(Bundle.TreeComparator_Count_displayName()) { @Override - public int compare(TreeItem> o1, TreeItem> o2) { + public int compare(TreeItem o1, TreeItem o2) { return Long.compare(o2.getValue().getCount(), o1.getValue().getCount()); } }, Type(Bundle.TreeComparator_Type_displayName()) { @Override - public int compare(TreeItem> o1, TreeItem> o2) { + public int compare(TreeItem o1, TreeItem o2) { return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType()); } }; From 64d3f09118e165ece629258279623eb251152279 Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 23 Feb 2016 13:40:26 -0500 Subject: [PATCH 04/35] support pinning of all events, not just SingleEvents --- .../autopsy/timeline/TimeLineController.java | 17 ++-- .../autopsy/timeline/datamodel/Event.java | 40 --------- .../timeline/datamodel/MultiEvent.java | 4 +- .../timeline/datamodel/SingleEvent.java | 4 +- .../timeline/datamodel/TimeLineEvent.java | 53 ++++++++++++ .../ui/detailview/DetailViewPane.java | 10 +-- .../timeline/ui/detailview/EventAxis.java | 6 +- .../ui/detailview/EventClusterNode.java | 27 ++----- .../timeline/ui/detailview/EventNodeBase.java | 79 ++++++++++++++---- .../ui/detailview/EventStripeNode.java | 8 +- .../ui/detailview/MultiEventNodeBase.java | 32 +++----- .../ui/detailview/PinnedEventsChart.java | 81 ++++++++++++------- .../ui/detailview/SingleEventNode.java | 61 ++++++-------- .../ui/detailview/StripeFlattener.java | 6 +- .../tree/EventDescriptionTreeItem.java | 14 ++-- .../ui/detailview/tree/EventTypeTreeItem.java | 10 +-- .../ui/detailview/tree/EventsTree.java | 16 ++-- .../ui/detailview/tree/NavTreeItem.java | 8 +- .../timeline/ui/detailview/tree/RootItem.java | 10 +-- .../ui/detailview/tree/TreeComparator.java | 10 +-- 20 files changed, 271 insertions(+), 225 deletions(-) delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/datamodel/Event.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index af1198d6f5..0a88c968c7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -74,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; @@ -291,19 +292,19 @@ public class TimeLineController { advance(filteredEvents.zoomParametersProperty().get().withTimeRange(boundingEventsInterval)); } - private final ObservableSet pinnedEventIDs = FXCollections.observableSet(); - private final ObservableSet pinnedEventIDsUnmodifiable = FXCollections.unmodifiableObservableSet(pinnedEventIDs); + private final ObservableSet pinnedEvents = FXCollections.observableSet(); + private final ObservableSet pinnedEventsUnmodifiable = FXCollections.unmodifiableObservableSet(pinnedEvents); - public void pinEvents(Collection eventIDs) { - pinnedEventIDs.addAll(eventIDs); + public void pinEvent(TimeLineEvent event) { + pinnedEvents.add(event); } - public void unPinEvents(Collection eventIDs) { - pinnedEventIDs.removeAll(eventIDs); + public void unPinEvent(TimeLineEvent event) { + pinnedEvents.removeIf(event::equals); } - public ObservableSet getPinnedEventIDs() { - return pinnedEventIDsUnmodifiable; + public ObservableSet getPinnedEvents() { + return pinnedEventsUnmodifiable; } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/Event.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/Event.java deleted file mode 100644 index 6f8e47dd94..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/Event.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.datamodel; - -import java.util.Set; -import java.util.SortedSet; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; - -/** - * - */ -public interface Event { - - public String getDescription(); - - public DescriptionLoD getDescriptionLoD(); - - Set getEventIDs(); - - Set getEventIDsWithHashHits(); - - Set getEventIDsWithTags(); - - EventType getEventType(); - - long getEndMillis(); - - long getStartMillis(); - - default int getCount() { - return getEventIDs().size(); - } - - - SortedSet getClusters(); -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java index 1c1ceaa73c..b928486fc5 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * A interface for groups of events that share some attributes in common. */ -public interface MultiEvent> extends Event { +public interface MultiEvent> extends TimeLineEvent { String getDescription(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/SingleEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/SingleEvent.java index b806fdfca4..94c425cf4c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/SingleEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/SingleEvent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014-15 Basis Technology Corp. + * Copyright 2014-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,7 +35,7 @@ import org.sleuthkit.datamodel.TskData; * A single event. */ @Immutable -public class SingleEvent implements Event { +public class SingleEvent implements TimeLineEvent { private final long eventID; private final long fileID; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java new file mode 100644 index 0000000000..b7711243b5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java @@ -0,0 +1,53 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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 java.util.Set; +import java.util.SortedSet; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; + +/** + * + */ +public interface TimeLineEvent { + + public String getDescription(); + + public DescriptionLoD getDescriptionLoD(); + + Set getEventIDs(); + + Set getEventIDsWithHashHits(); + + Set getEventIDsWithTags(); + + EventType getEventType(); + + long getEndMillis(); + + long getStartMillis(); + + default int getCount() { + return getEventIDs().size(); + } + + + SortedSet getClusters(); +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 9ed937e54c..684cf3e28e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014-15 Basis Technology Corp. + * Copyright 2014-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -69,7 +69,7 @@ import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.datamodel.Event; +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; @@ -100,7 +100,7 @@ public class DetailViewPane extends AbstractVisualizationPane> treeSelectionModel; + private MultipleSelectionModel> treeSelectionModel; private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); public ObservableList getEventStripes() { @@ -213,12 +213,12 @@ public class DetailViewPane extends AbstractVisualizationPane> selectionModel) { + public void setSelectionModel(MultipleSelectionModel> selectionModel) { this.treeSelectionModel = selectionModel; treeSelectionModel.getSelectedItems().addListener((Observable observable) -> { highlightedNodes.clear(); - for (TreeItem tn : treeSelectionModel.getSelectedItems()) { + for (TreeItem tn : treeSelectionModel.getSelectedItems()) { for (EventNodeBase n : chart.getNodes((EventNodeBase t) -> t.getDescription().equals(tn.getValue().getDescription()))) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java index 518181deed..d68f390894 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,13 +22,13 @@ import java.util.Collections; import java.util.List; import javafx.scene.chart.Axis; import javafx.scene.chart.XYChart; -import org.sleuthkit.autopsy.timeline.datamodel.Event; +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 { +class EventAxis extends Axis { @Override public double getDisplayPosition(Type value) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index 4ad9b30a1b..3d9d9f3cfa 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2013-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -49,9 +49,9 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; -import org.sleuthkit.autopsy.timeline.datamodel.Event; 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; @@ -77,6 +77,7 @@ final public class EventClusterNode extends MultiEventNodeBase { -// TimeLineController controller = getChart().getController(); -// if (controller.getPinnedEventIDs().contains(eventID)) { -// new UnPinEventAction(controller, getEventCluster().getEventIDs()).handle(actionEvent); -// pinButton.setGraphic(new ImageView(PIN)); -// } else { -// new PinEventAction(controller, getEventCluster().getEventIDs()).handle(actionEvent); -// pinButton.setGraphic(new ImageView(UNPIN)); -// } -// }); -// configureActionButton(pinButton); -// } } } @@ -213,14 +198,14 @@ final public class EventClusterNode extends MultiEventNodeBase bundles = get(); //clear the existing subnodes - List transform = subNodes.stream().flatMap(new StripeFlattener()).collect(Collectors.toList()); - ((EventDetailsChart) getChart()).getEventStripes().removeAll(transform); + List transform = subNodes.stream().flatMap(new StripeFlattener()).collect(Collectors.toList()); +// ((EventDetailsChart) getChart()).getEventStripes().removeAll(transform); subNodes.clear(); if (bundles.isEmpty()) { getChildren().setAll(subNodePane, infoHBox); descLOD.set(getEventBundle().getDescriptionLoD()); } else { - ((EventDetailsChart) getChart()).getEventStripes().addAll(bundles); +// ((EventDetailsChart) getChart()).getEventStripes().addAll(bundles); subNodes.addAll(Lists.transform(bundles, EventClusterNode.this::createChildNode)); getChildren().setAll(new VBox(infoHBox, subNodePane)); descLOD.set(loadedDescriptionLoD); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java index a047fb3908..2c703a5e76 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -1,14 +1,27 @@ + /* - * 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. + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; @@ -17,6 +30,7 @@ import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; import javafx.event.EventHandler; import javafx.scene.Node; +import javafx.scene.control.Button; import javafx.scene.control.ButtonBase; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; @@ -28,6 +42,7 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.util.Duration; import org.controlsfx.control.action.Action; @@ -35,7 +50,7 @@ import org.controlsfx.control.action.ActionUtils; import org.joda.time.DateTime; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.TimeLineController; -import org.sleuthkit.autopsy.timeline.datamodel.Event; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; @@ -45,7 +60,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * */ -public abstract class EventNodeBase extends StackPane { +public abstract class EventNodeBase extends StackPane { static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N NON-NLS static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N @@ -58,7 +73,7 @@ public abstract class EventNodeBase extends StackPane { private final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); - final Type ievent; + final Type tlEvent; final EventNodeBase parentNode; final Label descrLabel = new Label(); @@ -81,7 +96,7 @@ public abstract class EventNodeBase extends StackPane { public EventNodeBase(Type ievent, EventNodeBase parent, DetailsChart chart) { this.chart = chart; - this.ievent = ievent; + this.tlEvent = ievent; this.parentNode = parent; descVisibility.addListener(observable -> setDescriptionVisibiltiyImpl(descVisibility.get())); @@ -106,13 +121,45 @@ public abstract class EventNodeBase extends StackPane { } public Type getEvent() { - return ievent; + return tlEvent; } 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("", new ImageView(PIN)); + infoHBox.getChildren().add(pinButton); + configureActionButton(pinButton); + + pinButton.setOnAction(actionEvent -> { + TimeLineController controller = getChart().getController(); + if (controller.getPinnedEvents().contains(tlEvent)) { + new UnPinEventAction(controller, tlEvent).handle(actionEvent); + pinButton.setGraphic(new ImageView(PIN)); + } else { + new PinEventAction(controller, tlEvent).handle(actionEvent); + pinButton.setGraphic(new ImageView(UNPIN)); + } + }); + + } + } + + 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 Button pinButton; + abstract public TimeLineChart getChart(); void showHoverControls(final boolean showControls) { @@ -121,6 +168,8 @@ public abstract class EventNodeBase extends StackPane { setEffect(showControls ? dropShadow : null); installTooltip(); enableTooltip(showControls); + installActionButtons(); + show(pinButton, showControls); if (parentNode != null) { parentNode.showHoverControls(false); } @@ -137,11 +186,11 @@ public abstract class EventNodeBase extends StackPane { } EventType getEventType() { - return ievent.getEventType(); + return tlEvent.getEventType(); } long getStartMillis() { - return ievent.getStartMillis(); + return tlEvent.getStartMillis(); } final double getLayoutXCompensation() { @@ -196,9 +245,9 @@ public abstract class EventNodeBase extends StackPane { static class PinEventAction extends Action { @NbBundle.Messages({"PinEventAction.text=Pin"}) - PinEventAction(TimeLineController controller, Set eventIds) { + PinEventAction(TimeLineController controller, TimeLineEvent event) { super(Bundle.PinEventAction_text()); - setEventHandler(actionEvent -> controller.pinEvents(eventIds)); + setEventHandler(actionEvent -> controller.pinEvent(event)); setGraphic(new ImageView(PIN)); } } @@ -206,9 +255,9 @@ public abstract class EventNodeBase extends StackPane { static class UnPinEventAction extends Action { @NbBundle.Messages({"UnPinEventAction.text=Unpin"}) - UnPinEventAction(TimeLineController controller, Set eventIds) { + UnPinEventAction(TimeLineController controller, TimeLineEvent event) { super(Bundle.UnPinEventAction_text()); - setEventHandler(actionEvent -> controller.unPinEvents(eventIds)); + setEventHandler(actionEvent -> controller.unPinEvent(event)); setGraphic(new ImageView(UNPIN)); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 98822c0f25..1bf7c1e946 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -1,8 +1,7 @@ /* - * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,8 +56,9 @@ final public class EventStripeNode extends MultiEventNodeBase getActions() { - return Arrays.asList(new HideDescriptionAction(getDescription(), ievent.getDescriptionLoD(), chart)); + return Arrays.asList(new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chart)); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java index d6db0de0ce..e603d75c1b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,9 +38,7 @@ import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; -import javafx.scene.control.Label; import javafx.scene.control.Tooltip; -import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; @@ -49,7 +47,6 @@ 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.paint.Color; import javafx.util.Duration; @@ -59,8 +56,8 @@ 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.MultiEvent; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; @@ -95,11 +92,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent> subNodes = FXCollections.observableArrayList(); final Pane subNodePane = new Pane(); - 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; @@ -144,7 +137,6 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent { - Tooltip.uninstall(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); showHoverControls(true); toFront(); @@ -176,16 +168,10 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent hashSetCounts = new HashMap<>(); - if (ievent.getEventIDsWithHashHits().isEmpty() == false) { + if (tlEvent.getEventIDsWithHashHits().isEmpty() == false) { try { //TODO:push this to DB - for (SingleEvent tle : eventsModel.getEventsById(ievent.getEventIDsWithHashHits())) { + for (SingleEvent tle : eventsModel.getEventsById(tlEvent.getEventIDsWithHashHits())) { Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); for (String hashSetName : hashSetNames) { hashSetCounts.merge(hashSetName, 1L, Long::sum); @@ -230,8 +216,8 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent tagCounts = new HashMap<>(); - if (ievent.getEventIDsWithTags().isEmpty() == false) { - tagCounts.putAll(eventsModel.getTagCountsByTagName(ievent.getEventIDsWithTags())); + if (tlEvent.getEventIDsWithTags().isEmpty() == false) { + tagCounts.putAll(eventsModel.getTagCountsByTagName(tlEvent.getEventIDsWithTags())); } String tagCountsString = tagCounts.entrySet().stream() .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java index c567985fa6..cff64727e1 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java @@ -1,7 +1,20 @@ /* - * 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. + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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; @@ -33,9 +46,11 @@ import org.controlsfx.control.action.ActionUtils; 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.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; @@ -43,7 +58,7 @@ import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; /** * */ -public final class PinnedEventsChart extends XYChart implements DetailsChart { +public final class PinnedEventsChart extends XYChart implements DetailsChart { @Override public ContextMenu getChartContextMenu() { @@ -74,12 +89,12 @@ public final class PinnedEventsChart extends XYChart impl */ private final Group nodeGroup = new Group(); @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - private final ObservableList events = FXCollections.observableArrayList(); - private final ObservableList< SingleEventNode> eventNodes = FXCollections.observableArrayList(); - private final ObservableList< SingleEventNode> sortedEventNodes = eventNodes.sorted(Comparator.comparing(SingleEventNode::getStartMillis)); + private final ObservableList events = FXCollections.observableArrayList(); + private final ObservableList< EventNodeBase> eventNodes = FXCollections.observableArrayList(); + private final ObservableList< EventNodeBase> sortedEventNodes = eventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)); private double descriptionWidth; - Map eventMap = new HashMap<>(); + Map> eventMap = new HashMap<>(); /** * @@ -88,7 +103,7 @@ public final class PinnedEventsChart extends XYChart impl * @param verticalAxis the value of verticalAxis * @param selectedNodes1 the value of selectedNodes1 */ - PinnedEventsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis) { + PinnedEventsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis) { super(dateAxis, verticalAxis); this.controller = controller; @@ -116,7 +131,7 @@ public final class PinnedEventsChart extends XYChart impl //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly getPlotChildren().add(nodeGroup); - final Series series = new Series<>(); + final Series series = new Series<>(); setData(FXCollections.observableArrayList()); getData().add(series); // //add listener for events that should trigger layout @@ -138,17 +153,17 @@ public final class PinnedEventsChart extends XYChart impl // setOnMouseDragged(chartDragHandler); // // setOnMouseClicked(new MouseClickedHandler<>(this)); - controller.getPinnedEventIDs().addListener((SetChangeListener.Change change) -> { + controller.getPinnedEvents().addListener((SetChangeListener.Change change) -> { if (change.wasAdded()) { - SingleEvent eventById = controller.getEventsModel().getEventById(change.getElementAdded()); - Data data1 = new Data<>(new DateTime(eventById.getStartMillis()), eventById); + TimeLineEvent elementAdded = change.getElementAdded(); + Data data1 = new Data<>(new DateTime(elementAdded.getStartMillis()), elementAdded); series.getData().add(data1); addDataItem(data1); } if (change.wasRemoved()) { - final SingleEvent eventById = controller.getEventsModel().getEventById(change.getElementRemoved()); - Data data1 = new Data<>(new DateTime(eventById.getStartMillis()), eventById); - series.getData().removeIf(t -> eventById.equals(t.getYValue())); + TimeLineEvent elementRemoved = change.getElementRemoved(); + Data data1 = new Data<>(new DateTime(elementRemoved.getStartMillis()), elementRemoved); + series.getData().removeIf(t -> elementRemoved.equals(t.getYValue())); removeDataItem(data1); } @@ -193,7 +208,6 @@ public final class PinnedEventsChart extends XYChart impl public TimeLineController getController() { return controller; } - @Override public void requestTimelineChartLayout() { @@ -206,23 +220,23 @@ public final class PinnedEventsChart extends XYChart impl } @Override - protected void dataItemAdded(Series series, int itemIndex, Data item) { + protected void dataItemAdded(Series series, int itemIndex, Data item) { } @Override - protected void dataItemRemoved(Data item, Series series) { + protected void dataItemRemoved(Data item, Series series) { } @Override - protected void dataItemChanged(Data item) { + protected void dataItemChanged(Data item) { } @Override - protected void seriesAdded(Series series, int seriesIndex) { + protected void seriesAdded(Series series, int seriesIndex) { } @Override - protected void seriesRemoved(Series series) { + protected void seriesRemoved(Series series) { } /** * the maximum y value used so far during the most recent layout pass @@ -249,7 +263,7 @@ public final class PinnedEventsChart extends XYChart impl // .collect(Collectors.groupingBy(EventStripeNode::getEventType)).values() // .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); // } else { - maxY.set(layoutEventBundleNodes(sortedEventNodes.sorted(Comparator.comparing(SingleEventNode::getStartMillis)), 0)); + maxY.set(layoutEventBundleNodes(sortedEventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); // } setCursor(null); } @@ -346,11 +360,11 @@ public final class PinnedEventsChart extends XYChart impl * * @param data */ - void addDataItem(Data data) { - final SingleEvent event = data.getYValue(); + void addDataItem(Data data) { + final TimeLineEvent event = data.getYValue(); - SingleEventNode eventNode = new SingleEventNode(PinnedEventsChart.this, event, null); - eventMap.put(event.getEventID(), eventNode); + EventNodeBase eventNode = createNode(PinnedEventsChart.this, event); + eventMap.put(event, eventNode); Platform.runLater(() -> { events.add(event); eventNodes.add(eventNode); @@ -367,8 +381,8 @@ public final class PinnedEventsChart extends XYChart impl * * @param data */ - void removeDataItem(Data data) { - SingleEventNode removedNode = eventMap.remove(data.getYValue().getEventID()); + void removeDataItem(Data data) { + EventNodeBase removedNode = eventMap.remove(data.getYValue()); Platform.runLater(() -> { events.removeAll(data.getYValue()); eventNodes.removeAll(removedNode); @@ -377,4 +391,13 @@ public final class PinnedEventsChart extends XYChart impl }); } + static private EventNodeBase createNode(PinnedEventsChart chart, TimeLineEvent event) { + 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); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java index 57219054a9..9ef3f8bd45 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java @@ -1,7 +1,20 @@ /* - * 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. + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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; @@ -13,7 +26,6 @@ import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.Button; import javafx.scene.control.OverrunStyle; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; @@ -24,7 +36,6 @@ 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 static javafx.scene.layout.Region.USE_PREF_SIZE; import javafx.scene.paint.Color; import org.apache.commons.lang3.StringUtils; @@ -33,7 +44,6 @@ import org.joda.time.DateTime; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configureActionButton; import static org.sleuthkit.autopsy.timeline.ui.detailview.MultiEventNodeBase.CORNER_RADII_3; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; @@ -46,9 +56,7 @@ final class SingleEventNode extends EventNodeBase { final Background defaultBackground; private final Color evtColor; - final ImageView hashIV = new ImageView(HASH_PIN); - final ImageView tagIV = new ImageView(TAG); - final HBox infoHBox = new HBox(5, descrLabel, hashIV, tagIV); + static void show(Node b, boolean show) { b.setVisible(show); @@ -57,7 +65,7 @@ final class SingleEventNode extends EventNodeBase { static final CornerRadii CORNER_RADII_1 = new CornerRadii(1); private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(0, 0, 0, 2); private final ImageView eventTypeImageView = new ImageView(); - private Button pinButton; + @Override EventHandler getDoubleClickHandler() { @@ -68,10 +76,10 @@ final class SingleEventNode extends EventNodeBase { @Override Collection getActions() { TimeLineController controller = getChart().getController(); - if (controller.getPinnedEventIDs().contains(ievent.getEventID())) { - return Arrays.asList(new UnPinEventAction(controller, ievent.getEventIDs())); + if (controller.getPinnedEvents().contains(tlEvent)) { + return Arrays.asList(new UnPinEventAction(controller, tlEvent)); } else { - return Arrays.asList(new PinEventAction(controller, ievent.getEventIDs())); + return Arrays.asList(new PinEventAction(controller, tlEvent)); } } @@ -117,31 +125,12 @@ final class SingleEventNode extends EventNodeBase { return Collections.emptyList(); } - void installActionButtons() { - if (pinButton == null) { - pinButton = new Button("", new ImageView(PIN)); - infoHBox.getChildren().add(pinButton); - configureActionButton(pinButton); - - pinButton.setOnAction(actionEvent -> { - TimeLineController controller = getChart().getController(); - if (controller.getPinnedEventIDs().contains(ievent.getEventID())) { - new UnPinEventAction(controller, ievent.getEventIDs()).handle(actionEvent); - pinButton.setGraphic(new ImageView(PIN)); - } else { - new PinEventAction(controller, ievent.getEventIDs()).handle(actionEvent); - pinButton.setGraphic(new ImageView(UNPIN)); - } - }); - - } - } + @Override void showHoverControls(final boolean showControls) { super.showHoverControls(showControls); - installActionButtons(); - show(pinButton, showControls); + } @Override @@ -156,7 +145,7 @@ final class SingleEventNode extends EventNodeBase { @Override String getDescription() { - return ievent.getFullDescription(); + return tlEvent.getFullDescription(); } @Override @@ -186,7 +175,7 @@ final class SingleEventNode extends EventNodeBase { break; default: case SHOWN: - String description = ievent.getFullDescription(); + String description = tlEvent.getFullDescription(); description = parentNode != null ? " ..." + StringUtils.substringAfter(description, parentNode.getDescription()) : description; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java index 4baa792fa8..a2af78c4b4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java @@ -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.Event; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; /** * Use this recursive function to flatten a tree of nodes into an single stream. @@ -28,10 +28,10 @@ import org.sleuthkit.autopsy.timeline.datamodel.Event; * EventStripes containing the stripes for the given node and all child * eventStripes, ignoring intervening EventCluster nodes. */ -class StripeFlattener implements Function, Stream> { +class StripeFlattener implements Function, Stream> { @Override - public Stream apply(EventNodeBase node) { + public Stream apply(EventNodeBase node) { return Stream.concat( Stream.of(node.getEvent()), node.getSubNodes().stream().flatMap(clusterNode -> diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java index 57a8378ca2..1c6629ec5a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -26,7 +26,7 @@ import javafx.collections.FXCollections; import javafx.scene.control.TreeItem; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent; -import org.sleuthkit.autopsy.timeline.datamodel.Event; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; /** * @@ -37,14 +37,14 @@ class EventDescriptionTreeItem extends NavTreeItem { * maps a description to the child item of this item with that description */ private final Map childMap = new HashMap<>(); - private final Event bundle; - private Comparator> comparator = TreeComparator.Description; + private final TimeLineEvent bundle; + private Comparator> comparator = TreeComparator.Description; - public Event getEvent() { + public TimeLineEvent getEvent() { return bundle; } - EventDescriptionTreeItem(Event g, Comparator> comp) { + EventDescriptionTreeItem(TimeLineEvent g, Comparator> comp) { bundle = g; comparator = comp; setValue(g); @@ -85,7 +85,7 @@ class EventDescriptionTreeItem extends NavTreeItem { } @Override - void resort(Comparator> comp, Boolean recursive) { + void resort(Comparator> comp, Boolean recursive) { this.comparator = comp; FXCollections.sort(getChildren(), comp); if (recursive) { @@ -94,7 +94,7 @@ class EventDescriptionTreeItem extends NavTreeItem { } @Override - public NavTreeItem findTreeItemForEvent(Event t) { + public NavTreeItem findTreeItemForEvent(TimeLineEvent t) { if (getValue().getEventType() == t.getEventType() && getValue().getDescription().equals(t.getDescription())) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java index 0021732a28..a3bd708806 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -26,7 +26,7 @@ import javafx.collections.FXCollections; import javafx.scene.control.TreeItem; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent; -import org.sleuthkit.autopsy.timeline.datamodel.Event; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; class EventTypeTreeItem extends NavTreeItem { @@ -35,9 +35,9 @@ class EventTypeTreeItem extends NavTreeItem { */ private final Map childMap = new HashMap<>(); - private Comparator> comparator = TreeComparator.Description; + private Comparator> comparator = TreeComparator.Description; - EventTypeTreeItem(MultiEvent g, Comparator> comp) { + EventTypeTreeItem(MultiEvent g, Comparator> comp) { setValue(g); comparator = comp; } @@ -79,7 +79,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public NavTreeItem findTreeItemForEvent(Event t) { + public NavTreeItem findTreeItemForEvent(TimeLineEvent t) { if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) { for (EventDescriptionTreeItem child : childMap.values()) { @@ -93,7 +93,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - void resort(Comparator> comp, Boolean recursive) { + void resort(Comparator> comp, Boolean recursive) { this.comparator = comp; FXCollections.sort(getChildren(), comp); if (recursive) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java index a70cfee48a..e7ae8bb403 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java @@ -49,7 +49,7 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent; -import org.sleuthkit.autopsy.timeline.datamodel.Event; +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.detailview.DetailViewPane; @@ -67,7 +67,7 @@ final public class EventsTree extends BorderPane { private DetailViewPane detailViewPane; @FXML - private TreeView eventsTree; + private TreeView eventsTree; @FXML private Label eventsTreeLabel; @@ -146,7 +146,7 @@ final public class EventsTree extends BorderPane { * A tree cell to display {@link MultiEvent}s. Shows the description, and * count, as well a a "legend icon" for the event type. */ - private class EventBundleTreeCell extends TreeCell { + private class EventBundleTreeCell extends TreeCell { private static final double HIDDEN_MULTIPLIER = .6; private final Rectangle rect = new Rectangle(24, 24); @@ -161,7 +161,7 @@ final public class EventsTree extends BorderPane { } @Override - protected void updateItem(Event item, boolean empty) { + protected void updateItem(TimeLineEvent item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { setText(null); @@ -186,7 +186,7 @@ final public class EventsTree extends BorderPane { } else { setDisable(false); text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS - TreeItem parent = getTreeItem().getParent(); + TreeItem parent = getTreeItem().getParent(); if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) { text = StringUtils.substringAfter(text, parent.getValue().getDescription()); } @@ -214,7 +214,7 @@ final public class EventsTree extends BorderPane { } } - private void registerListeners(Collection filters, Event item) { + private void registerListeners(Collection filters, TimeLineEvent item) { for (DescriptionFilter filter : filters) { if (filter.getDescription().equals(item.getDescription())) { filter.activeProperty().addListener(filterStateChangeListener); @@ -230,8 +230,8 @@ final public class EventsTree extends BorderPane { } } - private void updateHiddenState(Event item) { - TreeItem treeItem = getTreeItem(); + private void updateHiddenState(TimeLineEvent item) { + TreeItem treeItem = getTreeItem(); hidden.set(controller.getQuickHideFilters().stream(). filter(AbstractFilter::isActive) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java index bda411df23..c3fd30ff9b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java @@ -20,7 +20,7 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.Event; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; /** * A node in the nav tree. Manages inserts and resorts. Has parents and @@ -28,12 +28,12 @@ import org.sleuthkit.autopsy.timeline.datamodel.Event; * {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type, * description , count, etc. */ -abstract class NavTreeItem extends TreeItem { +abstract class NavTreeItem extends TreeItem { abstract long getCount(); - abstract void resort(Comparator> comp, Boolean recursive); + abstract void resort(Comparator> comp, Boolean recursive); - abstract NavTreeItem findTreeItemForEvent(Event t); + abstract NavTreeItem findTreeItemForEvent(TimeLineEvent t); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java index 644c9d0e0f..499289fc67 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -27,7 +27,7 @@ import java.util.Optional; import javafx.scene.control.TreeItem; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent; -import org.sleuthkit.autopsy.timeline.datamodel.Event; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; /** @@ -43,9 +43,9 @@ class RootItem extends NavTreeItem { /** * the comparator if any used to sort the children of this item */ - private Comparator> comparator = TreeComparator.Type.reversed(); + private Comparator> comparator = TreeComparator.Type.reversed(); - RootItem(Comparator> comp) { + RootItem(Comparator> comp) { this.comparator = comp; } @@ -99,13 +99,13 @@ class RootItem extends NavTreeItem { } @Override - void resort(Comparator> comp, Boolean recursive) { + void resort(Comparator> comp, Boolean recursive) { comparator = comp; childMap.values().forEach(ti -> ti.resort(comp, true)); } @Override - public NavTreeItem findTreeItemForEvent(Event t) { + public NavTreeItem findTreeItemForEvent(TimeLineEvent t) { for (EventTypeTreeItem child : childMap.values()) { final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); if (findTreeItemForEvent != null) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java index 317f723cb9..08e027c8b3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java @@ -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.Event; +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> { +enum TreeComparator implements Comparator> { Description(Bundle.TreeComparator_Description_displayName()) { @Override - public int compare(TreeItem o1, TreeItem o2) { + public int compare(TreeItem o1, TreeItem o2) { return o1.getValue().getDescription().compareTo(o2.getValue().getDescription()); } }, Count(Bundle.TreeComparator_Count_displayName()) { @Override - public int compare(TreeItem o1, TreeItem o2) { + public int compare(TreeItem o1, TreeItem o2) { return Long.compare(o2.getValue().getCount(), o1.getValue().getCount()); } }, Type(Bundle.TreeComparator_Type_displayName()) { @Override - public int compare(TreeItem o1, TreeItem o2) { + public int compare(TreeItem o1, TreeItem o2) { return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType()); } }; From a6bfbf467af263fdb453a9cd93bb5da6ab6252c5 Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 23 Feb 2016 17:01:10 -0500 Subject: [PATCH 05/35] flesh out pinning multievents, and small ui changes to streamline bundles with only one sub-bundle. move eventTypeImageView into EventNodeBase --- .../timeline/datamodel/MultiEvent.java | 2 +- .../timeline/datamodel/TimeLineEvent.java | 2 +- .../ui/detailview/EventClusterNode.java | 51 +++--- .../timeline/ui/detailview/EventNodeBase.java | 149 ++++++++++++------ .../ui/detailview/EventStripeNode.java | 102 ++++++------ .../ui/detailview/MultiEventNodeBase.java | 43 +---- .../ui/detailview/PinnedEventsChart.java | 4 +- .../ui/detailview/SingleEventNode.java | 53 +++---- .../tree/EventDescriptionTreeItem.java | 2 +- .../ui/detailview/tree/EventTypeTreeItem.java | 2 +- .../ui/detailview/tree/EventsTree.java | 2 +- .../timeline/ui/detailview/tree/RootItem.java | 2 +- .../ui/detailview/tree/TreeComparator.java | 2 +- 13 files changed, 203 insertions(+), 213 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java index b928486fc5..c5e17b5542 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java @@ -47,7 +47,7 @@ public interface MultiEvent> extends TimeLineEv Optional getParentBundle(); - default int getCount() { + default int getSize() { return getEventIDs().size(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java index b7711243b5..f0a2c53334 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java @@ -44,7 +44,7 @@ public interface TimeLineEvent { long getStartMillis(); - default int getCount() { + default int getSize() { return getEventIDs().size(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index 3d9d9f3cfa..4cc3ac8d62 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -56,7 +56,6 @@ 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.EventNodeBase.configureActionButton; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; @@ -72,8 +71,8 @@ final public class EventClusterNode extends MultiEventNodeBase createChildNode(EventStripe stripe) { -// return new EventStripeNode(getChart(), stripe, this); - if (stripe.getEventIDs().size() == 1) { return new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(stripe.getEventIDs())), this); } else { @@ -270,7 +265,7 @@ final public class EventClusterNode extends MultiEventNodeBase new ExpandClusterAction().handle(null); } - private class ExpandClusterAction extends Action { + class ExpandClusterAction extends Action { @NbBundle.Messages({"ExpandClusterAction.text=Expand"}) ExpandClusterAction() { @@ -286,7 +281,7 @@ final public class EventClusterNode extends MultiEventNodeBase extends StackPan static final Map dropShadowMap = new ConcurrentHashMap<>(); - private final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); - - final Type tlEvent; - - final EventNodeBase parentNode; - final Label descrLabel = new Label(); - final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); - final SimpleObjectProperty descVisibility = new SimpleObjectProperty<>(); - static void configureActionButton(ButtonBase b) { b.setMinSize(16, 16); b.setMaxSize(16, 16); b.setPrefSize(16, 16); - show(b, false); +// show(b, false); } static void show(Node b, boolean show) { b.setVisible(show); b.setManaged(show); } - private Timeline timeline; - final DetailsChart chart; - public EventNodeBase(Type ievent, EventNodeBase parent, DetailsChart chart) { + final Type tlEvent; + + final EventNodeBase parentNode; + + final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + final SimpleObjectProperty descVisibility = new SimpleObjectProperty<>(); + + final DetailsChart chart; + 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_PIN); + final ImageView tagIV = new ImageView(TAG); + + final HBox controlsHBox = new HBox(5); + final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV, controlsHBox); + + private final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); + + private Timeline timeline; + private Button pinButton; + private final Border SELECTION_BORDER; + final ImageView eventTypeImageView = new ImageView(); + + EventNodeBase(Type ievent, EventNodeBase parent, DetailsChart chart) { this.chart = chart; this.tlEvent = ievent; this.parentNode = parent; + eventTypeImageView.setImage(getEventType().getFXImage()); + descrLabel.setGraphic(eventTypeImageView); + + if (chart.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)); descVisibility.addListener(observable -> setDescriptionVisibiltiyImpl(descVisibility.get())); // descVisibility.set(DescriptionVisibility.SHOWN); //trigger listener for initial value + setBackground(defaultBackground); //set up mouse hover effect and tooltip setOnMouseEntered(mouseEntered -> { - Tooltip.uninstall(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); showHoverControls(true); toFront(); @@ -118,50 +156,50 @@ public abstract class EventNodeBase extends StackPan } }); setOnMouseClicked(new ClickHandler()); + show(controlsHBox, false); } public Type getEvent() { return tlEvent; } + public abstract TimeLineChart getChart(); + + /** + * @param w the maximum width the description label should have + */ + public void setMaxDescriptionWidth(double w) { + descrLabel.setMaxWidth(w); + } + + public abstract List> 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 + * 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("", new ImageView(PIN)); - infoHBox.getChildren().add(pinButton); + pinButton = new Button(); + controlsHBox.getChildren().add(pinButton); configureActionButton(pinButton); - - pinButton.setOnAction(actionEvent -> { - TimeLineController controller = getChart().getController(); - if (controller.getPinnedEvents().contains(tlEvent)) { - new UnPinEventAction(controller, tlEvent).handle(actionEvent); - pinButton.setGraphic(new ImageView(PIN)); - } else { - new PinEventAction(controller, tlEvent).handle(actionEvent); - pinButton.setGraphic(new ImageView(UNPIN)); - } - }); - } } - 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 Button pinButton; - - abstract public TimeLineChart getChart(); - void showHoverControls(final boolean showControls) { Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(), eventType -> new DropShadow(-10, eventType.getColor())); @@ -169,7 +207,24 @@ public abstract class EventNodeBase extends StackPan installTooltip(); enableTooltip(showControls); installActionButtons(); - show(pinButton, showControls); + + TimeLineController controller = getChart().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); } @@ -185,7 +240,7 @@ public abstract class EventNodeBase extends StackPan } } - EventType getEventType() { + final EventType getEventType() { return tlEvent.getEventType(); } @@ -222,10 +277,6 @@ public abstract class EventNodeBase extends StackPan abstract void setDescriptionVisibiltiyImpl(DescriptionVisibility get); - abstract void setMaxDescriptionWidth(double descriptionWidth); - - abstract public List> getSubNodes(); - abstract void applyHighlightEffect(boolean b); void applyHighlightEffect() { @@ -238,9 +289,9 @@ public abstract class EventNodeBase extends StackPan abstract Collection getEventIDs(); - void applySelectionEffect(Boolean selected) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } + abstract EventHandler getDoubleClickHandler(); + + abstract Collection getActions(); static class PinEventAction extends Action { @@ -300,8 +351,4 @@ public abstract class EventNodeBase extends StackPan } - abstract EventHandler getDoubleClickHandler(); - - abstract Collection getActions(); - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 1bf7c1e946..f0418ed20e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -25,7 +25,6 @@ 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; @@ -44,73 +43,47 @@ final public class EventStripeNode extends MultiEventNodeBase createChildNode(EventCluster cluster) { - if (cluster.getEventIDs().size() == 1) { - return new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); + if (eventStripe.getClusters().size() > 1) { + for (EventCluster cluster : eventStripe.getClusters()) { + subNodes.add(createChildNode(cluster)); + } + getChildren().addAll(new VBox(infoHBox, subNodePane)); } else { - return new EventClusterNode(getChart(), cluster, this); - } - } + EventNodeBase childNode; + EventCluster cluster = Iterables.getOnlyElement(eventStripe.getClusters()); + if (cluster.getEventIDs().size() == 1) { + SingleEventNode singleEventNode = new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); + childNode = singleEventNode; + } else { + EventClusterNode eventClusterNode = new EventClusterNode(getChart(), cluster, this); + eventClusterNode.installActionButtons(); + eventClusterNode.infoHBox.getChildren().remove(eventClusterNode.countLabel); + controlsHBox.getChildren().addAll(eventClusterNode.minusButton, eventClusterNode.plusButton); + childNode = eventClusterNode; + } - @Override - void showHoverControls(final boolean showControls) { - super.showHoverControls(showControls); - installActionButtons(); - show(hideButton, showControls); + childNode.setDescriptionVisibiltiyImpl(DescriptionVisibility.HIDDEN); + subNodes.add(childNode); + getChildren().addAll(infoHBox, subNodePane); + } } public EventStripe getEventStripe() { return getEventBundle(); } - /** - * @param w the maximum width the description label should have - */ - @Override - public void setMaxDescriptionWidth(double w) { - descrLabel.setMaxWidth(w); - } - /** * apply the 'effect' to visually indicate highlighted nodes * @@ -127,9 +100,32 @@ final public class EventStripeNode extends MultiEventNodeBase createChildNode(EventCluster cluster) { + if (cluster.getEventIDs().size() == 1) { + return new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); + } else { + return new EventClusterNode(getChart(), cluster, this); + } + } + + + @Override void setDescriptionVisibiltiyImpl(DescriptionVisibility descrVis) { - final int size = getEventStripe().getCount(); + final int size = getEventStripe().getSize(); switch (descrVis) { case HIDDEN: diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java index e603d75c1b..d26a99151d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java @@ -40,15 +40,8 @@ import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.control.Tooltip; 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.Pane; -import javafx.scene.paint.Color; import javafx.util.Duration; import org.controlsfx.control.action.Action; import org.joda.time.DateTime; @@ -59,7 +52,6 @@ import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; @@ -80,32 +72,21 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent> subNodes = FXCollections.observableArrayList(); final Pane subNodePane = new Pane(); - - private final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); private Timeline timeline; - public MultiEventNodeBase(DetailsChart chart, BundleType eventBundle, ParentNodeType parentNode) { + MultiEventNodeBase(DetailsChart chart, BundleType eventBundle, ParentNodeType parentNode) { super(eventBundle, parentNode, 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); } @@ -113,7 +94,6 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent createChildNode(ParentType rawChild); - /** - * @param w the maximum width the description label should have - */ - abstract void setMaxDescriptionWidth(double w); - void animateTo(double xLeft, double yTop) { if (timeline != null) { timeline.stop(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java index cff64727e1..9fcbbc5dc9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java @@ -346,8 +346,8 @@ public final class PinnedEventsChart extends XYChart im eventNode.setVisible(true); eventNode.setManaged(true); //apply advanced layout description visibility options -// eventNode.setDescriptionVisibility(descrVisibility.get()); -// eventNode.setMaxDescriptionWidth(descriptionWidth); + eventNode.setDescriptionVisibility(DescriptionVisibility.SHOWN); + eventNode.setMaxDescriptionWidth(USE_PREF_SIZE); //do recursive layout eventNode.layoutChildren(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java index 9ef3f8bd45..2a501bd212 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java @@ -23,29 +23,23 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import javafx.event.EventHandler; -import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.OverrunStyle; import javafx.scene.image.ImageView; 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 static javafx.scene.layout.Region.USE_PREF_SIZE; -import javafx.scene.paint.Color; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.action.Action; import org.joda.time.DateTime; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; -import static org.sleuthkit.autopsy.timeline.ui.detailview.MultiEventNodeBase.CORNER_RADII_3; -import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; /** * @@ -53,10 +47,6 @@ import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; final class SingleEventNode extends EventNodeBase { private final DetailsChart chart; - final Background defaultBackground; - private final Color evtColor; - - static void show(Node b, boolean show) { b.setVisible(show); @@ -65,7 +55,6 @@ final class SingleEventNode extends EventNodeBase { static final CornerRadii CORNER_RADII_1 = new CornerRadii(1); private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(0, 0, 0, 2); private final ImageView eventTypeImageView = new ImageView(); - @Override EventHandler getDoubleClickHandler() { @@ -100,19 +89,12 @@ final class SingleEventNode extends EventNodeBase { show(tagIV, false); } - if (chart.getController().getEventsModel().getEventTypeZoom() == EventTypeZoomLevel.SUB_TYPE) { - evtColor = getEventType().getColor(); - } else { - evtColor = getEventType().getBaseType().getColor(); - } final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); setBorder(clusterBorder); - defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY)); - setBackground(defaultBackground); + setMaxWidth(USE_PREF_SIZE); infoHBox.setMaxWidth(USE_PREF_SIZE); getChildren().add(infoHBox); - } @Override @@ -125,14 +107,6 @@ final class SingleEventNode extends EventNodeBase { return Collections.emptyList(); } - - - @Override - void showHoverControls(final boolean showControls) { - super.showHoverControls(showControls); - - } - @Override protected void layoutChildren() { super.layoutChildren(); //To change body of generated methods, choose Tools | Templates. @@ -153,14 +127,28 @@ final class SingleEventNode extends EventNodeBase { chart.requestTimelineChartLayout(); } + /** + * apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ @Override - void applyHighlightEffect(boolean b) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + 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); + } } + /** + * @param w the maximum width the description label should have + */ @Override - void setMaxDescriptionWidth(double descriptionWidth) { -// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + public void setMaxDescriptionWidth(double w) { + descrLabel.setMaxWidth(w); } @Override @@ -168,13 +156,16 @@ final class SingleEventNode extends EventNodeBase { switch (descrVis) { case HIDDEN: + countLabel.setText(null); descrLabel.setText(""); break; case COUNT_ONLY: + countLabel.setText(null); descrLabel.setText(""); break; default: case SHOWN: + countLabel.setText(null); String description = tlEvent.getFullDescription(); description = parentNode != null ? " ..." + StringUtils.substringAfter(description, parentNode.getDescription()) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java index 1c6629ec5a..85372311f7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -52,7 +52,7 @@ class EventDescriptionTreeItem extends NavTreeItem { @Override public long getCount() { - return getValue().getCount(); + return getValue().getSize(); } @ThreadConfined(type = ThreadConfined.ThreadType.JFX) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java index a3bd708806..8f25e296b0 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -44,7 +44,7 @@ class EventTypeTreeItem extends NavTreeItem { @Override public long getCount() { - return getValue().getCount(); + return getValue().getSize(); } @ThreadConfined(type = ThreadConfined.ThreadType.JFX) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java index e7ae8bb403..7a79b4946d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java @@ -185,7 +185,7 @@ final public class EventsTree extends BorderPane { setDisable(true); } else { setDisable(false); - text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS + text = item.getDescription() + " (" + item.getSize() + ")"; // NON-NLS TreeItem parent = getTreeItem().getParent(); if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) { text = StringUtils.substringAfter(text, parent.getValue().getDescription()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java index 499289fc67..11cc15f11d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -51,7 +51,7 @@ class RootItem extends NavTreeItem { @Override public long getCount() { - return getValue().getCount(); + return getValue().getSize(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java index 08e027c8b3..f30a43fc58 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java @@ -38,7 +38,7 @@ enum TreeComparator implements Comparator> { Count(Bundle.TreeComparator_Count_displayName()) { @Override public int compare(TreeItem o1, TreeItem o2) { - return Long.compare(o2.getValue().getCount(), o1.getValue().getCount()); + return Long.compare(o2.getValue().getSize(), o1.getValue().getSize()); } }, Type(Bundle.TreeComparator_Type_displayName()) { From 38af730c584ce59b02bcb6e8280ec1aea65a095d Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 24 Feb 2016 14:07:24 -0500 Subject: [PATCH 06/35] make pinned events scrollable --- .../ui/detailview/DetailViewPane.java | 94 ++----- .../timeline/ui/detailview/DetailsChart.java | 5 + .../ui/detailview/EventDetailsChart.java | 6 +- .../ui/detailview/PinnedEventsChart.java | 240 +++++++++--------- .../ui/detailview/ScrollingWrapper.java | 99 ++++++++ 5 files changed, 246 insertions(+), 198 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingWrapper.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 684cf3e28e..b0490b143c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -40,25 +40,15 @@ 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.SplitPane; 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.controlsfx.control.action.Action; import org.joda.time.DateTime; @@ -91,17 +81,14 @@ public class DetailViewPane extends AbstractVisualizationPane verticalAxis = new EventAxis<>(); - private final ScrollBar vertScrollBar = new ScrollBar(); - private final Region scrollBarSpacer = new Region(); private MultipleSelectionModel> treeSelectionModel; private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ScrollingWrapper mainView; + private final ScrollingWrapper pinnedView; public ObservableList getEventStripes() { return chart.getEventStripes(); @@ -113,21 +100,21 @@ public class DetailViewPane extends AbstractVisualizationPane { - vertScrollBar.setValue(0); - }); + mainView.reset(); + pinnedView.reset(); } public DetailViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region bottomLeftSpacer) { super(controller, partPane, contextPane, bottomLeftSpacer); //initialize chart; chart = new EventDetailsChart(controller, detailsChartDateAxis, verticalAxis, selectedNodes); - + mainView = new ScrollingWrapper<>(chart); PinnedEventsChart pinnedChart = new PinnedEventsChart(controller, pinnedDateAxis, new EventAxis<>()); + pinnedView = new ScrollingWrapper<>(pinnedChart); pinnedChart.setMinSize(100, 100); setChartClickHandler(); //can we push this into chart - SplitPane splitPane = new SplitPane(pinnedChart, chart); + SplitPane splitPane = new SplitPane(pinnedView, mainView); splitPane.setOrientation(Orientation.VERTICAL); chart.setData(dataSeries); setCenter(splitPane); @@ -142,50 +129,6 @@ public class DetailViewPane extends AbstractVisualizationPane - 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> change) -> { while (change.next()) { @@ -196,32 +139,25 @@ public class DetailViewPane extends AbstractVisualizationPane { highlightedNodes.clear(); - selectedNodes.stream().forEach((tn) -> { - for (EventNodeBase n : chart.getNodes((EventNodeBase t) -> - t.getDescription().equals(tn.getDescription()))) { + for (EventNodeBase selectedNode : selectedNodes) { + String selectedDescription = selectedNode.getDescription(); + for (EventNodeBase n : chart.getNodes(eventNode -> + selectedDescription.equals(eventNode.getDescription()))) { highlightedNodes.add(n); } - }); + } }); } - 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)); - } - public void setSelectionModel(MultipleSelectionModel> selectionModel) { this.treeSelectionModel = selectionModel; treeSelectionModel.getSelectedItems().addListener((Observable observable) -> { highlightedNodes.clear(); for (TreeItem tn : treeSelectionModel.getSelectedItems()) { - - for (EventNodeBase n : chart.getNodes((EventNodeBase t) -> - t.getDescription().equals(tn.getValue().getDescription()))) { + String description = tn.getValue().getDescription(); + for (EventNodeBase n : chart.getNodes(eventNode -> + description.equals(eventNode.getDescription()))) { highlightedNodes.add(n); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java index 063a3f368d..a25410837b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java @@ -6,6 +6,7 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.Collection; +import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.scene.Node; @@ -37,6 +38,10 @@ interface DetailsChart extends TimeLineChart { double layoutEventBundleNodes(final Collection> nodes, final double minY); + ReadOnlyDoubleProperty maxVScrollProperty(); + + void setVScroll(double vScrollValue); + @NbBundle.Messages({"HideDescriptionAction.displayName=Hide", "HideDescriptionAction.displayMsg=Hide this group from the details view."}) static class HideDescriptionAction extends Action { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java index 0d70965f21..738c6f5c13 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java @@ -97,7 +97,7 @@ import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; public final class EventDetailsChart extends XYChart implements DetailsChart { private static final String styleSheet = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //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; @@ -459,7 +459,7 @@ public final class EventDetailsChart extends XYChart impl setCursor(null); } - ReadOnlyDoubleProperty maxVScrollProperty() { + public ReadOnlyDoubleProperty maxVScrollProperty() { return maxY.getReadOnlyProperty(); } @@ -483,7 +483,7 @@ public final class EventDetailsChart extends XYChart impl .filter(p).collect(Collectors.toList()); } - synchronized void setVScroll(double vScrollValue) { + public synchronized void setVScroll(double vScrollValue) { nodeGroup.setTranslateY(-vScrollValue); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java index 9fcbbc5dc9..2732e1cb40 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.Map; import java.util.MissingResourceException; import javafx.application.Platform; +import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -60,26 +61,21 @@ import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; */ public final class PinnedEventsChart extends XYChart implements DetailsChart { - @Override - public ContextMenu getChartContextMenu() { - return chartContextMenu; - } - private ContextMenu chartContextMenu; - - @Override - public ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException { - if (chartContextMenu != null) { - chartContextMenu.hide(); - } - - chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(//new EventDetailsChart.PlaceMarkerAction(clickEvent), - TimeLineChart.newZoomHistoyActionGroup(controller))); - chartContextMenu.setAutoHide(true); - return chartContextMenu; - } -// final ObservableList> selectedNodes; - private static final String styleSheet = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS + private static final int MINIMUM_EVENT_NODE_GAP = 4; + private static final int MINIMUM_ROW_HEIGHT = 24; + + private static EventNodeBase createNode(PinnedEventsChart chart, TimeLineEvent event) { + 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); + } + } + Map> eventMap = new HashMap<>(); + private ContextMenu chartContextMenu; private final TimeLineController controller; private final FilteredEventsModel filteredEvents; @@ -94,7 +90,10 @@ public final class PinnedEventsChart extends XYChart im private final ObservableList< EventNodeBase> sortedEventNodes = eventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)); private double descriptionWidth; - Map> eventMap = new HashMap<>(); + /** + * the maximum y value used so far during the most recent layout pass + */ + private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); /** * @@ -174,6 +173,24 @@ public final class PinnedEventsChart extends XYChart im // selectedNodes.addListener(new SelectionChangeHandler()); } + @Override + public ContextMenu getChartContextMenu() { + return chartContextMenu; + } + + @Override + public ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException { + if (chartContextMenu != null) { + chartContextMenu.hide(); + } + + chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(//new EventDetailsChart.PlaceMarkerAction(clickEvent), + TimeLineChart.newZoomHistoyActionGroup(controller))); + chartContextMenu.setAutoHide(true); + return chartContextMenu; + } +// final ObservableList> selectedNodes; + @Override public Node asNode() { return this; @@ -219,62 +236,6 @@ public final class PinnedEventsChart extends XYChart im return FXCollections.observableArrayList(); } - @Override - protected void dataItemAdded(Series series, int itemIndex, Data item) { - } - - @Override - protected void dataItemRemoved(Data item, Series series) { - } - - @Override - protected void dataItemChanged(Data item) { - } - - @Override - protected void seriesAdded(Series series, int seriesIndex) { - } - - @Override - protected void seriesRemoved(Series series) { - } - /** - * the maximum y value used so far during the most recent layout pass - */ - private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); - - @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(sortedEventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); -// } - setCursor(null); - } - private static final int MINIMUM_EVENT_NODE_GAP = 4; - private final static int MINIMUM_ROW_HEIGHT = 24; - - private double getXForEpochMillis(Long millis) { - DateTime dateTime = new DateTime(millis); - return getXAxis().getDisplayPosition(dateTime); - } - @Override public double layoutEventBundleNodes(final Collection> nodes, final double minY) { // map from y-ranges to maximum x @@ -309,6 +270,51 @@ public final class PinnedEventsChart extends XYChart im return localMax; //return new max } + @Override + protected void dataItemAdded(Series series, int itemIndex, Data item) { + } + + @Override + protected void dataItemRemoved(Data item, Series series) { + } + + @Override + protected void dataItemChanged(Data item) { + } + + @Override + protected void seriesAdded(Series series, int seriesIndex) { + } + + @Override + protected void seriesRemoved(Series series) { + } + + @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(sortedEventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); +// } + setCursor(null); + } + /** * expose as protected */ @@ -317,40 +323,11 @@ public final class PinnedEventsChart extends XYChart im super.requestChartLayout(); } - private double computeYTop(double yMin, double h, TreeRangeMap 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; + public synchronized void setVScroll(double vScrollValue) { + nodeGroup.setTranslateY(-vScrollValue); } - - private void layoutBundleHelper(final EventNodeBase eventNode) { - //make sure it is shown - eventNode.setVisible(true); - eventNode.setManaged(true); - //apply advanced layout description visibility options - eventNode.setDescriptionVisibility(DescriptionVisibility.SHOWN); - eventNode.setMaxDescriptionWidth(USE_PREF_SIZE); - - //do recursive layout - eventNode.layoutChildren(); + public ReadOnlyDoubleProperty maxVScrollProperty() { + return maxY.getReadOnlyProperty(); } /** @@ -391,13 +368,44 @@ public final class PinnedEventsChart extends XYChart im }); } - static private EventNodeBase createNode(PinnedEventsChart chart, TimeLineEvent event) { - 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); + private double getXForEpochMillis(Long millis) { + DateTime dateTime = new DateTime(millis); + return getXAxis().getDisplayPosition(dateTime); + } + + private double computeYTop(double yMin, double h, TreeRangeMap 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; + } + + private void layoutBundleHelper(final EventNodeBase eventNode) { + //make sure it is shown + eventNode.setVisible(true); + eventNode.setManaged(true); + //apply advanced layout description visibility options + eventNode.setDescriptionVisibility(DescriptionVisibility.SHOWN); + eventNode.setMaxDescriptionWidth(USE_PREF_SIZE); + + //do recursive layout + eventNode.layoutChildren(); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingWrapper.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingWrapper.java new file mode 100644 index 0000000000..11ca4fe780 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingWrapper.java @@ -0,0 +1,99 @@ +/* + * 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.detailview; + +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.geometry.Orientation; +import javafx.scene.chart.XYChart; +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; +import org.joda.time.DateTime; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; + +/** + * + */ +public class ScrollingWrapper & DetailsChart> 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 ChartType chart; + + public ScrollingWrapper(ChartType center) { + super(center); + this.chart = center; + + scrollBarSpacer.minHeightProperty().bind(((XYChart) 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(new InvalidationListener() { + + public void invalidated(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)); + } +} From 4acaed2557aec30e349002ca524de62ac1719eef Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 24 Feb 2016 15:28:43 -0500 Subject: [PATCH 07/35] PnnedEventsChart is better integrated interms of respecting settings and selection --- .../detailview/DetailViewLayoutSettings.java | 91 ++++++++++++++++++ .../ui/detailview/DetailViewPane.java | 21 +++-- .../ui/detailview/EventClusterNode.java | 6 -- .../ui/detailview/EventDetailsChart.java | 93 ++++--------------- .../timeline/ui/detailview/EventNodeBase.java | 15 ++- .../ui/detailview/EventStripeNode.java | 16 +--- .../ui/detailview/MultiEventNodeBase.java | 7 -- .../ui/detailview/PinnedEventsChart.java | 59 +++++++----- .../ui/detailview/SingleEventNode.java | 16 +--- 9 files changed, 172 insertions(+), 152 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewLayoutSettings.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewLayoutSettings.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewLayoutSettings.java new file mode 100644 index 0000000000..b8485c3590 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewLayoutSettings.java @@ -0,0 +1,91 @@ +/* + * 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.detailview; + +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; + +/** + * + */ +public class DetailViewLayoutSettings { + + /** + * 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 descrVisibility = + new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); + + public synchronized SimpleBooleanProperty bandByTypeProperty() { + return bandByType; + } + + 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(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index b0490b143c..b841c35d73 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -89,6 +89,7 @@ public class DetailViewPane extends AbstractVisualizationPane> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); private final ScrollingWrapper mainView; private final ScrollingWrapper pinnedView; + private final DetailViewLayoutSettings layoutSettings; public ObservableList getEventStripes() { return chart.getEventStripes(); @@ -107,10 +108,12 @@ public class DetailViewPane extends AbstractVisualizationPane(chart); - PinnedEventsChart pinnedChart = new PinnedEventsChart(controller, pinnedDateAxis, new EventAxis<>()); + PinnedEventsChart pinnedChart = new PinnedEventsChart(controller, pinnedDateAxis, new EventAxis<>(), selectedNodes, layoutSettings); pinnedView = new ScrollingWrapper<>(pinnedChart); pinnedChart.setMinSize(100, 100); setChartClickHandler(); //can we push this into chart @@ -270,14 +273,14 @@ public class DetailViewPane extends AbstractVisualizationPane { if (truncateWidthSlider.isValueChanging() == false) { - chart.getTruncateWidth().set(truncateWidthSlider.getValue()); + layoutSettings.truncateWidthProperty().set(truncateWidthSlider.getValue()); } }; truncateWidthSlider.valueProperty().addListener(sliderListener); @@ -285,11 +288,11 @@ public class DetailViewPane extends AbstractVisualizationPane { 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); } }); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index 4cc3ac8d62..a62f230e4c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -101,16 +101,10 @@ final public class EventClusterNode extends MultiEventNodeBase impl private Set activeQuickHidefilters; @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass private double descriptionWidth; + private final DetailViewLayoutSettings layoutSettings; @Override public ContextMenu getChartContextMenu() { @@ -157,40 +155,9 @@ public final class EventDetailsChart extends XYChart impl private final ObservableList< EventNodeBase> sortedStripeNodes = stripeNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)); private final Map 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 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 verticalAxis, ObservableList> selectedNodes) { + EventDetailsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes, DetailViewLayoutSettings layoutSettings) { super(dateAxis, verticalAxis); + this.layoutSettings = layoutSettings; this.controller = controller; this.filteredEvents = this.controller.getEventsModel(); @@ -224,11 +191,11 @@ public final class EventDetailsChart extends XYChart impl 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); + layoutSettings.bandByTypeProperty().addListener(layoutInvalidationListener); + layoutSettings.oneEventPerRowProperty().addListener(layoutInvalidationListener); + layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); + layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); + layoutSettings.descrVisibilityProperty().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 @@ -289,19 +256,11 @@ public final class EventDetailsChart extends XYChart impl intervalSelector = null; } - public synchronized SimpleBooleanProperty bandByTypeProperty() { - return bandByType; - } - @Override public IntervalSelector 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} @@ -326,22 +285,6 @@ public final class EventDetailsChart extends XYChart impl 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 * @@ -446,9 +389,9 @@ public final class EventDetailsChart extends XYChart impl .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; + descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE; - if (bandByType.get()) { + if (layoutSettings.getBandByType()) { sortedStripeNodes.stream() .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); @@ -547,7 +490,7 @@ public final class EventDetailsChart extends XYChart impl double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP; //initial test position - double yTop = (oneEventPerRow.get()) + double yTop = (layoutSettings.getOneEventPerRow()) ? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end : computeYTop(minY, h, maxXatY, xLeft, xRight); @@ -607,19 +550,19 @@ public final class EventDetailsChart extends XYChart impl * * Set layout paramaters on the given node and layout its children * - * @param bundleNode the Node to layout + * @param eventNode the Node to layout * @param descriptionWdith the maximum width for the description text */ - private void layoutBundleHelper(final EventNodeBase< ?> bundleNode) { + private void layoutBundleHelper(final EventNodeBase< ?> eventNode) { //make sure it is shown - bundleNode.setVisible(true); - bundleNode.setManaged(true); + eventNode.setVisible(true); + eventNode.setManaged(true); //apply advanced layout description visibility options - bundleNode.setDescriptionVisibility(descrVisibility.get()); - bundleNode.setMaxDescriptionWidth(descriptionWidth); + eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility()); + eventNode.setMaxDescriptionWidth(descriptionWidth); //do recursive layout - bundleNode.layoutChildren(); + eventNode.layoutChildren(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java index 156cdfe72b..a9e111c73a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -277,7 +277,20 @@ public abstract class EventNodeBase extends StackPan abstract void setDescriptionVisibiltiyImpl(DescriptionVisibility get); - abstract void applyHighlightEffect(boolean b); + /** + * 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); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index f0418ed20e..100be6b0d2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -84,21 +84,7 @@ final public class EventStripeNode extends MultiEventNodeBase> getSubNodes() { return subNodes; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java index 2732e1cb40..e28b80c2c6 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java @@ -26,7 +26,10 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.MissingResourceException; +import java.util.stream.Collectors; 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; @@ -94,6 +97,14 @@ public final class PinnedEventsChart extends XYChart im * the maximum y value used so far during the most recent layout pass */ private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); + private final ObservableList> selectedNodes; + private final DetailViewLayoutSettings layoutSettings; + /** + * listener that triggers chart layout pass + */ + private final InvalidationListener layoutInvalidationListener = (Observable o) -> { + layoutPlotChildren(); + }; /** * @@ -102,9 +113,9 @@ public final class PinnedEventsChart extends XYChart im * @param verticalAxis the value of verticalAxis * @param selectedNodes1 the value of selectedNodes1 */ - PinnedEventsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis) { + PinnedEventsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes, DetailViewLayoutSettings layoutSettings) { super(dateAxis, verticalAxis); - + this.layoutSettings = layoutSettings; this.controller = controller; this.filteredEvents = this.controller.getEventsModel(); @@ -134,11 +145,12 @@ public final class PinnedEventsChart extends XYChart im setData(FXCollections.observableArrayList()); getData().add(series); // //add listener for events that should trigger layout -// bandByType.addListener(layoutInvalidationListener); -// oneEventPerRow.addListener(layoutInvalidationListener); -// truncateAll.addListener(layoutInvalidationListener); -// truncateWidth.addListener(layoutInvalidationListener); -// descrVisibility.addListener(layoutInvalidationListener); + layoutSettings.bandByTypeProperty().addListener(layoutInvalidationListener); + layoutSettings.oneEventPerRowProperty().addListener(layoutInvalidationListener); + layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); + layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); + layoutSettings.descrVisibilityProperty().addListener(layoutInvalidationListener); + getController().getQuickHideFilters().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 @@ -168,9 +180,7 @@ public final class PinnedEventsChart extends XYChart im requestChartLayout(); }); - -// this.selectedNodes = selectedNodes; -// selectedNodes.addListener(new SelectionChangeHandler()); + this.selectedNodes = selectedNodes; } @Override @@ -233,7 +243,7 @@ public final class PinnedEventsChart extends XYChart im @Override public ObservableList> getSelectedNodes() { - return FXCollections.observableArrayList(); + return selectedNodes; } @Override @@ -257,7 +267,9 @@ public final class PinnedEventsChart extends XYChart im double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP; //initial test position - double yTop = computeYTop(minY, h, maxXatY, xLeft, xRight); + 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); @@ -301,17 +313,15 @@ public final class PinnedEventsChart extends XYChart im // .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; + descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : 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(sortedEventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); -// } + if (layoutSettings.getBandByType()) { + sortedEventNodes.stream() + .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() + .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); + } else { + maxY.set(layoutEventBundleNodes(sortedEventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); + } setCursor(null); } @@ -326,6 +336,7 @@ public final class PinnedEventsChart extends XYChart im public synchronized void setVScroll(double vScrollValue) { nodeGroup.setTranslateY(-vScrollValue); } + public ReadOnlyDoubleProperty maxVScrollProperty() { return maxY.getReadOnlyProperty(); } @@ -402,8 +413,8 @@ public final class PinnedEventsChart extends XYChart im eventNode.setVisible(true); eventNode.setManaged(true); //apply advanced layout description visibility options - eventNode.setDescriptionVisibility(DescriptionVisibility.SHOWN); - eventNode.setMaxDescriptionWidth(USE_PREF_SIZE); + eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility()); + eventNode.setMaxDescriptionWidth(descriptionWidth); //do recursive layout eventNode.layoutChildren(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java index 2a501bd212..a209545e45 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java @@ -127,21 +127,7 @@ final class SingleEventNode extends EventNodeBase { chart.requestTimelineChartLayout(); } - /** - * 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); - } - } + /** * @param w the maximum width the description label should have From 780a96f039ad0551529ea94e5bb2533d2f5902a3 Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 24 Feb 2016 15:34:01 -0500 Subject: [PATCH 08/35] selection in PinnedEventsChart populates dataresulttable --- .../autopsy/timeline/ui/detailview/DetailViewPane.java | 4 ++++ .../autopsy/timeline/ui/detailview/EventDetailsChart.java | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index b841c35d73..3d4b648777 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; @@ -149,6 +150,9 @@ public class DetailViewPane extends AbstractVisualizationPane detailNode.getEventIDs().stream()) + .collect(Collectors.toList())); }); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java index 012386ff14..4499047ca7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java @@ -678,10 +678,6 @@ public final class EventDetailsChart extends XYChart impl } }); } - EventDetailsChart.this.controller.selectEventIDs(selectedNodes.stream() - .flatMap(detailNode -> detailNode.getEventIDs().stream()) - .collect(Collectors.toList())); } } - } From 1abf6d721bcd5df37760a887280197ec032ed616 Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 24 Feb 2016 16:10:23 -0500 Subject: [PATCH 09/35] make selection highlighting more consistent across pinned and non-pinned event nodes --- .../ui/detailview/DetailViewPane.java | 19 ++++++++------- .../timeline/ui/detailview/EventNodeBase.java | 6 ++++- .../ui/detailview/PinnedEventsChart.java | 23 +++++++++++++++++++ 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 3d4b648777..81e9bf54cd 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -91,6 +91,7 @@ public class DetailViewPane extends AbstractVisualizationPane mainView; private final ScrollingWrapper pinnedView; private final DetailViewLayoutSettings layoutSettings; + private final PinnedEventsChart pinnedChart; public ObservableList getEventStripes() { return chart.getEventStripes(); @@ -114,7 +115,7 @@ public class DetailViewPane extends AbstractVisualizationPane(chart); - PinnedEventsChart pinnedChart = new PinnedEventsChart(controller, pinnedDateAxis, new EventAxis<>(), selectedNodes, layoutSettings); + pinnedChart = new PinnedEventsChart(controller, pinnedDateAxis, new EventAxis<>(), selectedNodes, layoutSettings); pinnedView = new ScrollingWrapper<>(pinnedChart); pinnedChart.setMinSize(100, 100); setChartClickHandler(); //can we push this into chart @@ -136,19 +137,15 @@ public class DetailViewPane extends AbstractVisualizationPane> change) -> { while (change.next()) { - change.getAddedSubList().forEach(EventNodeBase::applyHighlightEffect); - change.getRemoved().forEach(EventNodeBase::clearHighlightEffect); + change.getAddedSubList().forEach(eventNode -> eventNode.applyHighlightEffect()); + change.getRemoved().forEach(eventNode -> eventNode.clearHighlightEffect()); } }); selectedNodes.addListener((Observable observable) -> { highlightedNodes.clear(); for (EventNodeBase selectedNode : selectedNodes) { - String selectedDescription = selectedNode.getDescription(); - for (EventNodeBase n : chart.getNodes(eventNode -> - selectedDescription.equals(eventNode.getDescription()))) { - highlightedNodes.add(n); - } + highlightedNodes.add(selectedNode); } controller.selectEventIDs(selectedNodes.stream() .flatMap(detailNode -> detailNode.getEventIDs().stream()) @@ -163,8 +160,10 @@ public class DetailViewPane extends AbstractVisualizationPane tn : treeSelectionModel.getSelectedItems()) { String description = tn.getValue().getDescription(); - for (EventNodeBase n : chart.getNodes(eventNode -> - description.equals(eventNode.getDescription()))) { + for (EventNodeBase n : chart.getNodes(eventNode -> eventNode.hasDescription(description))) { + highlightedNodes.add(n); + } + for (EventNodeBase n : pinnedChart.getNodes(eventNode -> eventNode.hasDescription(description))) { highlightedNodes.add(n); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java index a9e111c73a..01e0ded42d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -277,12 +277,16 @@ public abstract class EventNodeBase extends StackPan abstract void setDescriptionVisibiltiyImpl(DescriptionVisibility get); + boolean hasDescription(String other) { + return this.getDescription().startsWith(other); + } + /** * 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) { + synchronized void applyHighlightEffect(boolean applied) { if (applied) { descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS setBackground(highlightedBackground); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java index e28b80c2c6..4bea6a9695 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java @@ -26,7 +26,10 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.MissingResourceException; +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; @@ -362,6 +365,26 @@ public final class PinnedEventsChart extends XYChart im }); } + /** + * @return all the nodes that pass the given predicate + */ + synchronized Iterable> getNodes(Predicate> p) { + //use this recursive function to flatten the tree of nodes into an single stream. + Function, Stream>> stripeFlattener = + new Function, Stream>>() { + @Override + public Stream> apply(EventNodeBase node) { + return Stream.concat( + Stream.of(node), + node.getSubNodes().stream().flatMap(this::apply)); + } + }; + + return sortedEventNodes.stream() + .flatMap(stripeFlattener) + .filter(p).collect(Collectors.toList()); + } + /** * remove a data item from this chart * From 0df9cdaabfea35ed4d0639bf2259f3186fd25935 Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 24 Feb 2016 16:20:50 -0500 Subject: [PATCH 10/35] hide tick marks, improve resizing of splitpane --- .../timeline/ui/detailview/DateAxis.java | 62 ++++++++++--------- .../ui/detailview/DetailViewPane.java | 4 +- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java index dd08141bc1..df1244596e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java @@ -50,7 +50,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; * @author Diego Cirujano */ final class DateAxis extends Axis { - + private ObjectProperty lowerBound = new ObjectPropertyBase() { @Override protected void invalidated() { @@ -59,12 +59,12 @@ final class DateAxis extends Axis { requestAxisLayout(); } } - + @Override public Object getBean() { return DateAxis.this; } - + @Override public String getName() { return "lowerBound"; // NON-NLS @@ -84,11 +84,11 @@ final class DateAxis extends Axis { * bounds. */ private DateTime minDate; - + private RangeDivisionInfo rangeDivisionInfo; - + private final ReadOnlyDoubleWrapper tickSpacing = new ReadOnlyDoubleWrapper(); - + private final ObjectProperty upperBound = new ObjectPropertyBase() { @Override protected void invalidated() { @@ -97,12 +97,12 @@ final class DateAxis extends Axis { requestAxisLayout(); } } - + @Override public Object getBean() { return DateAxis.this; } - + @Override public String getName() { return "upperBound"; // NON-NLS @@ -117,8 +117,10 @@ final class DateAxis extends Axis { setTickLabelGap(0); setAutoRanging(false); setTickLabelsVisible(false); + setTickLength(0); + setTickMarkVisible(false); } - + @Override public double getDisplayPosition(DateTime date) { final double length = -200 + (getSide().isHorizontal() ? getWidth() : getHeight()); @@ -149,7 +151,7 @@ final class DateAxis extends Axis { * * @see #lowerBoundProperty() */ - public final DateTime getLowerBound() { + public DateTime getLowerBound() { return lowerBound.get(); } @@ -160,7 +162,7 @@ final class DateAxis extends Axis { * * @see #lowerBoundProperty() */ - public final void setLowerBound(DateTime date) { + public void setLowerBound(DateTime date) { lowerBound.set(date); } @@ -171,7 +173,7 @@ final class DateAxis extends Axis { * * @see #upperBoundProperty() */ - public final DateTime getUpperBound() { + public DateTime getUpperBound() { return upperBound.get(); } @@ -182,10 +184,10 @@ final class DateAxis extends Axis { * * @see #upperBoundProperty() () */ - public final void setUpperBound(DateTime date) { + public void setUpperBound(DateTime date) { upperBound.set(date); } - + @Override public DateTime getValueForDisplay(double displayPosition) { final double length = - 200 + (getSide().isHorizontal() ? getWidth() : getHeight()); @@ -196,7 +198,7 @@ final class DateAxis extends Axis { // Get the actual range of the visible area. // The minimal date should start at the zero position, that's why we subtract it. double range = length - getZeroPosition(); - + if (getSide().isVertical()) { // displayPosition = getHeight() - ((date - lowerBound) / diff) * range + getZero // date = displayPosition - getZero - getHeight())/range * diff + lowerBound @@ -207,16 +209,16 @@ final class DateAxis extends Axis { return new DateTime((long) ((displayPosition - getZeroPosition()) / range * diff + getLowerBound().getMillis()), TimeLineController.getJodaTimeZone()); } } - + @Override public double getZeroPosition() { return 0; } - + @Override public void invalidateRange(List list) { super.invalidateRange(list); - + Collections.sort(list); if (list.isEmpty()) { minDate = maxDate = new DateTime(); @@ -227,22 +229,22 @@ final class DateAxis extends Axis { maxDate = list.get(list.size() - 1); } } - + @Override public boolean isValueOnAxis(DateTime date) { return date.getMillis() > getLowerBound().getMillis() && date.getMillis() < getUpperBound().getMillis(); } - + @Override public double toNumericValue(DateTime date) { return date.getMillis(); } - + @Override public DateTime toRealValue(double v) { return new DateTime((long) v); } - + @Override protected Interval autoRange(double length) { if (isAutoRanging()) { @@ -254,7 +256,7 @@ final class DateAxis extends Axis { return getRange(); } } - + @Override protected List calculateTickValues(double length, Object range) { List tickDates = new ArrayList<>(); @@ -264,13 +266,13 @@ final class DateAxis extends Axis { rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo((Interval) range); final DateTime lowerBound1 = getLowerBound(); final DateTime upperBound1 = getUpperBound(); - + if (lowerBound1 == null || upperBound1 == null) { return tickDates; } DateTime lower = lowerBound1.withZone(TimeLineController.getJodaTimeZone()); DateTime upper = upperBound1.withZone(TimeLineController.getJodaTimeZone()); - + DateTime current = lower; // Loop as long we exceeded the upper bound. while (current.isBefore(upper)) { @@ -302,7 +304,7 @@ final class DateAxis extends Axis { tickDates.remove(lastDate); } } - + if (tickDates.size() >= 2) { tickSpacing.set(getDisplayPosition(tickDates.get(1)) - getDisplayPosition(tickDates.get(0))); } else if (tickDates.size() >= 4) { @@ -310,17 +312,17 @@ final class DateAxis extends Axis { } return tickDates; } - + @Override protected Interval getRange() { return new Interval(getLowerBound(), getUpperBound()); } - + @Override protected String getTickMarkLabel(DateTime date) { return rangeDivisionInfo.getTickFormatter().print(date); } - + @Override protected void layoutChildren() { super.layoutChildren(); @@ -337,7 +339,7 @@ final class DateAxis extends Axis { setLowerBound(new DateTime(rangeDivisionInfo.getLowerBound(), TimeLineController.getJodaTimeZone())); setUpperBound(new DateTime(rangeDivisionInfo.getUpperBound(), TimeLineController.getJodaTimeZone())); } - + ReadOnlyDoubleProperty getTickSpacing() { return tickSpacing.getReadOnlyProperty(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 81e9bf54cd..ba90191da8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -117,10 +117,12 @@ public class DetailViewPane extends AbstractVisualizationPane(chart); pinnedChart = new PinnedEventsChart(controller, pinnedDateAxis, new EventAxis<>(), selectedNodes, layoutSettings); pinnedView = new ScrollingWrapper<>(pinnedChart); - pinnedChart.setMinSize(100, 100); + pinnedChart.setMinSize(65, 100); setChartClickHandler(); //can we push this into chart SplitPane splitPane = new SplitPane(pinnedView, mainView); splitPane.setOrientation(Orientation.VERTICAL); + splitPane.setDividerPositions(.1); + SplitPane.setResizableWithParent(pinnedView, Boolean.FALSE); chart.setData(dataSeries); setCenter(splitPane); From d0561dcbac72fa721106957a4fb007095d140bcf Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 24 Feb 2016 16:38:08 -0500 Subject: [PATCH 11/35] use masterDetailPane to allow hiding of pinned events move tooltip loading into EventNodeBase minor cleanup improve pinned events collapsing, keep minimum height --- .../ui/detailview/DetailViewPane.java | 36 ++++-- .../ui/detailview/DetailViewSettingsPane.fxml | 28 ++++- .../ui/detailview/EventClusterNode.java | 4 +- .../timeline/ui/detailview/EventNodeBase.java | 118 ++++++++++++++++-- .../ui/detailview/MultiEventNodeBase.java | 104 +-------------- .../ui/detailview/PinnedEventsChart.java | 2 +- .../ui/detailview/SingleEventNode.java | 13 +- 7 files changed, 168 insertions(+), 137 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index ba90191da8..7f74e550c9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -29,7 +29,7 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.fxml.FXML; -import javafx.geometry.Orientation; +import javafx.geometry.Side; import javafx.scene.chart.Axis; import javafx.scene.chart.XYChart; import javafx.scene.control.Alert; @@ -43,7 +43,7 @@ import javafx.scene.control.MultipleSelectionModel; import javafx.scene.control.RadioButton; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.Slider; -import javafx.scene.control.SplitPane; +import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.TreeItem; import javafx.scene.effect.Effect; @@ -51,6 +51,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.stage.Modality; +import org.controlsfx.control.MasterDetailPane; import org.controlsfx.control.action.Action; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -92,6 +93,9 @@ public class DetailViewPane extends AbstractVisualizationPane pinnedView; private final DetailViewLayoutSettings layoutSettings; private final PinnedEventsChart pinnedChart; + private final MasterDetailPane masterDetailPane; + private double dividerPosition = .1; + private static final int MIN_PINNED_LANE_HEIGHT = 50; public ObservableList getEventStripes() { return chart.getEventStripes(); @@ -117,14 +121,15 @@ public class DetailViewPane extends AbstractVisualizationPane(chart); pinnedChart = new PinnedEventsChart(controller, pinnedDateAxis, new EventAxis<>(), selectedNodes, layoutSettings); pinnedView = new ScrollingWrapper<>(pinnedChart); - pinnedChart.setMinSize(65, 100); + pinnedChart.setMinHeight(MIN_PINNED_LANE_HEIGHT); + pinnedView.setMinHeight(MIN_PINNED_LANE_HEIGHT); setChartClickHandler(); //can we push this into chart - SplitPane splitPane = new SplitPane(pinnedView, mainView); - splitPane.setOrientation(Orientation.VERTICAL); - splitPane.setDividerPositions(.1); - SplitPane.setResizableWithParent(pinnedView, Boolean.FALSE); + masterDetailPane = new MasterDetailPane(Side.TOP, mainView, pinnedView, false); + masterDetailPane.setDividerPosition(dividerPosition); +// SplitPane.setResizableWithParent(pinnedChart, Boolean.FALSE); +// SplitPane.setResizableWithParent(pinnedView, Boolean.FALSE); chart.setData(dataSeries); - setCenter(splitPane); + setCenter(masterDetailPane); settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable()); //bind layout fo axes and spacers @@ -268,6 +273,9 @@ public class DetailViewPane extends AbstractVisualizationPane { + boolean selected = pinnedEventsToggle.isSelected(); + if (selected == false) { + dividerPosition = masterDetailPane.getDividerPosition(); + } + masterDetailPane.setShowDetailNode(selected); + if (selected) { + pinnedView.setMinHeight(MIN_PINNED_LANE_HEIGHT); + masterDetailPane.setDividerPosition(dividerPosition); + } + }); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml index e6f801f2c0..350096d1aa 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml @@ -1,12 +1,30 @@ - - - - + + + + + + + + + + + + + - + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index a62f230e4c..5bb1e86c43 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -34,6 +34,7 @@ import javafx.event.EventHandler; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.Button; +import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Border; @@ -66,7 +67,8 @@ import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; final public class EventClusterNode extends MultiEventNodeBase { private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); - + 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 static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1); private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java index 01e0ded42d..ef93dba265 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -20,14 +20,20 @@ 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.property.SimpleObjectProperty; +import javafx.concurrent.Task; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Node; @@ -57,7 +63,11 @@ 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.ui.AbstractVisualizationPane; @@ -66,26 +76,27 @@ 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.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; /** * */ public abstract class EventNodeBase extends StackPane { - - static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N NON-NLS - static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N - static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N - static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N - static final Image PIN = new Image("/org/sleuthkit/autopsy/timeline/images/marker--plus.png"); // NON-NLS //NOI18N - static final Image UNPIN = new Image("/org/sleuthkit/autopsy/timeline/images/marker--minus.png"); // NON-NLS //NOI18N - static final Map dropShadowMap = new ConcurrentHashMap<>(); + private static final Logger LOGGER = Logger.getLogger(EventNodeBase.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 + + 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 + static void configureActionButton(ButtonBase b) { b.setMinSize(16, 16); b.setMaxSize(16, 16); b.setPrefSize(16, 16); -// show(b, false); } static void show(Node b, boolean show) { @@ -113,17 +124,22 @@ public abstract class EventNodeBase extends StackPan final HBox controlsHBox = new HBox(5); final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV, controlsHBox); - private final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); + final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); + final ImageView eventTypeImageView = new ImageView(); + final SleuthkitCase sleuthkitCase; + final FilteredEventsModel eventsModel; private Timeline timeline; private Button pinButton; private final Border SELECTION_BORDER; - final ImageView eventTypeImageView = new ImageView(); EventNodeBase(Type ievent, EventNodeBase parent, DetailsChart chart) { this.chart = chart; this.tlEvent = ievent; this.parentNode = parent; + + sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); + eventsModel = chart.getController().getEventsModel(); eventTypeImageView.setImage(getEventType().getFXImage()); descrLabel.setGraphic(eventTypeImageView); @@ -200,7 +216,7 @@ public abstract class EventNodeBase extends StackPan } } - void showHoverControls(final boolean showControls) { + final void showHoverControls(final boolean showControls) { Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(), eventType -> new DropShadow(-10, eventType.getColor())); setEffect(showControls ? dropShadow : null); @@ -230,7 +246,79 @@ public abstract class EventNodeBase extends StackPan } } - abstract void installTooltip(); + /** + * 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 tooltTipTask = new Task() { + { + updateTitle(Bundle.EventNodeBase_toolTip_loading2()); + } + + @Override + protected String call() throws Exception { + HashMap hashSetCounts = new HashMap<>(); + if (tlEvent.getEventIDsWithHashHits().isEmpty() == false) { + try { + //TODO:push this to DB + for (SingleEvent tle : eventsModel.getEventsById(tlEvent.getEventIDsWithHashHits())) { + Set 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 t) -> t.getKey() + " : " + t.getValue()) + .collect(Collectors.joining("\n")); + + Map tagCounts = new HashMap<>(); + if (tlEvent.getEventIDsWithTags().isEmpty() == false) { + tagCounts.putAll(eventsModel.getTagCountsByTagName(tlEvent.getEventIDsWithTags())); + } + String tagCountsString = tagCounts.entrySet().stream() + .map((Map.Entry 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(); + chart.getController().monitorTask(tooltTipTask); + } + } void enableTooltip(boolean toolTipEnabled) { if (toolTipEnabled) { @@ -248,6 +336,10 @@ public abstract class EventNodeBase extends StackPan return tlEvent.getStartMillis(); } + final long getEndMillis() { + return tlEvent.getEndMillis(); + } + final double getLayoutXCompensation() { return parentNode != null ? getChart().getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis())) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java index d05312baea..6e5a74710f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java @@ -19,13 +19,8 @@ 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.ExecutionException; -import java.util.logging.Level; -import java.util.stream.Collectors; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; @@ -33,7 +28,6 @@ import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.concurrent.Task; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Orientation; @@ -47,16 +41,10 @@ import org.controlsfx.control.action.Action; 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.MultiEvent; -import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; /** * @@ -66,26 +54,22 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent> extends EventNodeBase { private static final Logger LOGGER = Logger.getLogger(MultiEventNodeBase.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); - final SleuthkitCase sleuthkitCase; - final FilteredEventsModel eventsModel; + final ObservableList> subNodes = FXCollections.observableArrayList(); final Pane subNodePane = new Pane(); - private final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); + private Timeline timeline; MultiEventNodeBase(DetailsChart chart, BundleType eventBundle, ParentNodeType parentNode) { super(eventBundle, parentNode, chart); this.descLOD.set(eventBundle.getDescriptionLoD()); - sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); - eventsModel = chart.getController().getEventsModel(); + if (eventBundle.getEventIDsWithHashHits().isEmpty()) { show(hashIV, false); @@ -151,79 +135,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent tooltTipTask = new Task() { - { - updateTitle(Bundle.EventBundleNodeBase_toolTip_loading2()); - } - - @Override - protected String call() throws Exception { - HashMap hashSetCounts = new HashMap<>(); - if (tlEvent.getEventIDsWithHashHits().isEmpty() == false) { - try { - //TODO:push this to DB - for (SingleEvent tle : eventsModel.getEventsById(tlEvent.getEventIDsWithHashHits())) { - Set 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 t) -> t.getKey() + " : " + t.getValue()) - .collect(Collectors.joining("\n")); - - Map tagCounts = new HashMap<>(); - if (tlEvent.getEventIDsWithTags().isEmpty() == false) { - tagCounts.putAll(eventsModel.getTagCountsByTagName(tlEvent.getEventIDsWithTags())); - } - String tagCountsString = tagCounts.entrySet().stream() - .map((Map.Entry 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); - } - } + @SuppressWarnings("unchecked") public List> getSubNodes() { @@ -234,13 +146,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent getEventIDs() { return getEventBundle().getEventIDs(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java index 4bea6a9695..fe4840b27b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java @@ -80,7 +80,7 @@ public final class PinnedEventsChart extends XYChart im return new EventStripeNode(chart, (EventStripe) event, null); } } - Map> eventMap = new HashMap<>(); + private Map> eventMap = new HashMap<>(); private ContextMenu chartContextMenu; private final TimeLineController controller; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java index a209545e45..daf89d9a9c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java @@ -26,7 +26,6 @@ import javafx.event.EventHandler; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.OverrunStyle; -import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Border; import javafx.scene.layout.BorderStroke; @@ -37,6 +36,7 @@ import static javafx.scene.layout.Region.USE_PREF_SIZE; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.action.Action; import org.joda.time.DateTime; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; @@ -45,8 +45,7 @@ import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; * */ final class SingleEventNode extends EventNodeBase { - - private final DetailsChart chart; + private static final Logger LOGGER = Logger.getLogger(SingleEventNode.class.getName()); static void show(Node b, boolean show) { b.setVisible(show); @@ -54,7 +53,6 @@ final class SingleEventNode extends EventNodeBase { } static final CornerRadii CORNER_RADII_1 = new CornerRadii(1); private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(0, 0, 0, 2); - private final ImageView eventTypeImageView = new ImageView(); @Override EventHandler getDoubleClickHandler() { @@ -74,7 +72,6 @@ final class SingleEventNode extends EventNodeBase { SingleEventNode(DetailsChart chart, SingleEvent event, MultiEventNodeBase parent) { super(event, parent, chart); - this.chart = chart; this.descrLabel.setText(event.getFullDescription()); eventTypeImageView.setImage(getEventType().getFXImage()); descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); @@ -112,11 +109,7 @@ final class SingleEventNode extends EventNodeBase { super.layoutChildren(); //To change body of generated methods, choose Tools | Templates. } - @Override - void installTooltip() { -// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - + @Override String getDescription() { return tlEvent.getFullDescription(); From 651fac80ec9d2d203a9bb0e2beae646801246f81 Mon Sep 17 00:00:00 2001 From: jmillman Date: Fri, 26 Feb 2016 11:13:17 -0500 Subject: [PATCH 12/35] bind swinmlane max height to height needed to show all items --- .../autopsy/timeline/ui/detailview/DetailViewPane.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 7f74e550c9..947e0362dd 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -336,9 +336,11 @@ public class DetailViewPane extends AbstractVisualizationPane Date: Wed, 2 Mar 2016 10:28:17 -0500 Subject: [PATCH 13/35] show pinned events automatically, add labels to pinned and all event swim lanes move more code into DetailViewPane to allow guideline and interval selector to span the pinned and primary chart lanes pull common code into DetailsChartLane base class pull more common code into DetailsChartLane base class pull even more common code into DetailsChartLane base class --- .../ui/AbstractVisualizationPane.java | 4 + .../timeline/ui/ContextMenuProvider.java | 40 + .../autopsy/timeline/ui/IntervalSelector.java | 4 +- .../autopsy/timeline/ui/TimeLineChart.java | 36 +- .../ui/countsview/EventCountsChart.java | 6 +- .../ui/detailview/DetailViewPane.java | 194 ++++- .../timeline/ui/detailview/DetailsChart.java | 85 --- .../ui/detailview/DetailsChartLane.java | 456 ++++++++++++ .../timeline/ui/detailview/EventAxis.java | 4 + .../ui/detailview/EventClusterNode.java | 20 +- .../ui/detailview/EventDetailsChart.java | 683 ------------------ .../timeline/ui/detailview/EventNodeBase.java | 140 ++-- .../ui/detailview/EventStripeNode.java | 29 +- .../timeline/ui/detailview/GuideLine.java | 6 +- .../ui/detailview/MultiEventNodeBase.java | 17 +- .../ui/detailview/PinnedEventsChart.java | 402 ++--------- .../ui/detailview/PrimaryDetailsChart.java | 163 +++++ ...Wrapper.java => ScrollingLaneWrapper.java} | 21 +- .../ui/detailview/SingleEventNode.java | 12 +- 19 files changed, 1043 insertions(+), 1279 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java rename Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/{ScrollingWrapper.java => ScrollingLaneWrapper.java} (79%) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index 8a3ad0c784..9c4bbfa1d7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -132,6 +132,10 @@ public abstract class AbstractVisualizationPane settingsNodes; + public TimeLineController getController() { + return controller; + } + /** * @return the list of nodes containing settings widgets to insert into this * visualization's header diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java new file mode 100644 index 0000000000..b25d6f1972 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java @@ -0,0 +1,40 @@ +/* + * 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.chart.Axis; +import javafx.scene.control.ContextMenu; +import javafx.scene.input.MouseEvent; +import org.sleuthkit.autopsy.timeline.TimeLineController; + +public interface ContextMenuProvider { + + public Axis getXAxis(); + + public TimeLineController getController(); + + ContextMenu getContextMenu(); + + ContextMenu getChartContextMenu(MouseEvent m); + + IntervalSelector getIntervalSelector(); + + void setIntervalSelector(IntervalSelector newIntervalSelector); + + /** + * derived classes should implement this so as to supply an appropriate + * subclass of {@link IntervalSelector} + * + * @return a new interval selector + */ + IntervalSelector newIntervalSelector(); + + /** + * clear any references to previous interval selectors , including removing + * the interval selector from the ui / scene-graph + */ + void clearIntervalSelector(); +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java index 04869f3e0b..c8d38ca6ec 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java @@ -61,7 +61,7 @@ public abstract class IntervalSelector extends BorderPane { /** * the Axis this is a selector over */ - public final TimeLineChart chart; + public final ContextMenuProvider chart; private Tooltip tooltip; /////////drag state @@ -89,7 +89,7 @@ public abstract class IntervalSelector extends BorderPane { @FXML private BorderPane bottomBorder; - public IntervalSelector(TimeLineChart chart) { + public IntervalSelector(ContextMenuProvider chart) { this.chart = chart; this.controller = chart.getController(); FXMLConstructor.construct(this, IntervalSelector.class, "IntervalSelector.fxml"); // NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java index 0f34db262f..05ff1911b3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java @@ -22,10 +22,9 @@ import javafx.event.EventHandler; import javafx.event.EventType; import javafx.scene.Cursor; 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; @@ -37,11 +36,7 @@ import org.sleuthkit.autopsy.timeline.actions.Forward; * * @param the type of values along the horizontal axis */ -public interface TimeLineChart { - - - - +public interface TimeLineChart extends ContextMenuProvider { // void setController(TimeLineController controller); IntervalSelector getIntervalSelector(); @@ -66,11 +61,6 @@ public interface TimeLineChart { public TimeLineController getController(); - - ContextMenu getChartContextMenu(); - - ContextMenu getChartContextMenu(MouseEvent m); - /** * drag handler class used by {@link TimeLineChart}s to create * {@link IntervalSelector}s @@ -78,7 +68,7 @@ public interface TimeLineChart { * @param the type of values along the horizontal axis * @param the type of chart this is a drag handler for */ - static class ChartDragHandler> implements EventHandler { + public static class ChartDragHandler> implements EventHandler { private final Y chart; @@ -102,8 +92,7 @@ public interface TimeLineChart { chart.getIntervalSelector().prefHeightProperty().bind(chart.heightProperty()); startX = mouseEvent.getX(); chart.getIntervalSelector().relocate(startX, 0); - } else { - //resize/position existing selector + } else //resize/position existing selector if (mouseEvent.getX() > startX) { chart.getIntervalSelector().relocate(startX, 0); chart.getIntervalSelector().setPrefWidth(mouseEvent.getX() - startX); @@ -111,7 +100,6 @@ public interface TimeLineChart { chart.getIntervalSelector().relocate(mouseEvent.getX(), 0); chart.getIntervalSelector().setPrefWidth(startX - mouseEvent.getX()); } - } chart.getIntervalSelector().autosize(); } else if (mouseEventType == MouseEvent.MOUSE_RELEASED) { chart.setCursor(Cursor.DEFAULT); @@ -122,7 +110,7 @@ public interface TimeLineChart { } - static class MouseClickedHandler> implements EventHandler { + static class MouseClickedHandler> implements EventHandler { private final C chart; @@ -131,15 +119,15 @@ public interface TimeLineChart { } @Override - public void handle(MouseEvent clickEvent) { - if (chart.getChartContextMenu() != null) { - chart.getChartContextMenu().hide(); + public void handle(MouseEvent mouseEvent) { + if (chart.getContextMenu() != null) { + chart.getContextMenu().hide(); } - if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) { - chart.getChartContextMenu(clickEvent); + if (mouseEvent.getButton() == MouseButton.SECONDARY && mouseEvent.isStillSincePress()) { + chart.getChartContextMenu(mouseEvent); chart.setOnMouseMoved(this); - chart.getChartContextMenu().show(chart, clickEvent.getScreenX(), clickEvent.getScreenY()); - clickEvent.consume(); + chart.getContextMenu().show(chart, mouseEvent.getScreenX(), mouseEvent.getScreenY()); + mouseEvent.consume(); } } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java index 6e2cf15534..3d5980c12f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java @@ -65,10 +65,6 @@ final class EventCountsChart extends StackedBarChart 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; @@ -163,7 +159,7 @@ final class EventCountsChart extends StackedBarChart implements * * @return the context menu for this chart */ - ContextMenu getContextMenu() { + public ContextMenu getContextMenu() { return chartContextMenu; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 947e0362dd..31f1cc4ef1 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -19,7 +19,10 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.MissingResourceException; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.InvalidationListener; @@ -27,6 +30,7 @@ import javafx.beans.Observable; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import javafx.collections.SetChangeListener; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.geometry.Side; @@ -36,6 +40,7 @@ import javafx.scene.control.Alert; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; +import javafx.scene.control.ContextMenu; import javafx.scene.control.CustomMenuItem; import javafx.scene.control.Label; import javafx.scene.control.MenuButton; @@ -47,12 +52,16 @@ import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.TreeItem; import javafx.scene.effect.Effect; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.stage.Modality; 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; @@ -63,44 +72,93 @@ 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.ui.ContextMenuProvider; +import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; +import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChartLane.HideDescriptionAction; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChartLane.UnhideDescriptionAction; 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, EventDetailsChart> { +public class DetailViewPane extends AbstractVisualizationPane, PrimaryDetailsChart> implements ContextMenuProvider { private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName()); private final DateAxis detailsChartDateAxis = new DateAxis(); private final DateAxis pinnedDateAxis = new DateAxis(); - private final Axis verticalAxis = new EventAxis<>(); + private final Axis verticalAxis = new EventAxis<>("All Events"); private MultipleSelectionModel> treeSelectionModel; private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - private final ScrollingWrapper mainView; - private final ScrollingWrapper pinnedView; + private final ScrollingLaneWrapper mainView; + private final ScrollingLaneWrapper pinnedView; private final DetailViewLayoutSettings layoutSettings; private final PinnedEventsChart pinnedChart; private final MasterDetailPane masterDetailPane; private double dividerPosition = .1; private static final int MIN_PINNED_LANE_HEIGHT = 50; + private ContextMenu contextMenu; + private IntervalSelector intervalSelector; + private final Pane rootPane; public ObservableList getEventStripes() { return chart.getEventStripes(); } + private static class DetailIntervalSelector extends IntervalSelector { + + DetailIntervalSelector(ContextMenuProvider 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; + } + } + + @Override + public ContextMenu getContextMenu() { + return contextMenu; + } + + @Override + public ContextMenu getChartContextMenu(MouseEvent mouseEvent) throws MissingResourceException { + if (contextMenu != null) { + contextMenu.hide(); + } + + contextMenu = ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(mouseEvent), + TimeLineChart.newZoomHistoyActionGroup(controller))); + contextMenu.setAutoHide(true); + return contextMenu; + } + + DetailViewLayoutSettings getLayoutSettings() { + return layoutSettings; + } + @Override protected void resetData() { for (XYChart.Series s : dataSeries) { @@ -117,19 +175,22 @@ public class DetailViewPane extends AbstractVisualizationPane(chart); - pinnedChart = new PinnedEventsChart(controller, pinnedDateAxis, new EventAxis<>(), selectedNodes, layoutSettings); - pinnedView = new ScrollingWrapper<>(pinnedChart); + chart = new PrimaryDetailsChart(this, detailsChartDateAxis, verticalAxis); + chart.setData(dataSeries); + setChartClickHandler(); //can we push this into chart + + mainView = new ScrollingLaneWrapper(chart); + pinnedChart = new PinnedEventsChart(this, pinnedDateAxis, new EventAxis<>("Pinned Events")); + pinnedView = new ScrollingLaneWrapper(pinnedChart); pinnedChart.setMinHeight(MIN_PINNED_LANE_HEIGHT); pinnedView.setMinHeight(MIN_PINNED_LANE_HEIGHT); - setChartClickHandler(); //can we push this into chart + masterDetailPane = new MasterDetailPane(Side.TOP, mainView, pinnedView, false); masterDetailPane.setDividerPosition(dividerPosition); -// SplitPane.setResizableWithParent(pinnedChart, Boolean.FALSE); -// SplitPane.setResizableWithParent(pinnedView, Boolean.FALSE); - chart.setData(dataSeries); - setCenter(masterDetailPane); + masterDetailPane.prefHeightProperty().bind(heightProperty()); + masterDetailPane.prefWidthProperty().bind(widthProperty()); + rootPane = new Pane(masterDetailPane); + setCenter(rootPane); settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable()); //bind layout fo axes and spacers @@ -158,6 +219,40 @@ public class DetailViewPane extends AbstractVisualizationPane detailNode.getEventIDs().stream()) .collect(Collectors.toList())); }); + + filteredEvents.zoomParametersProperty().addListener(o -> { + clearIntervalSelector(); + selectedNodes.clear(); + controller.selectEventIDs(Collections.emptyList()); + }); + + TimeLineChart.MouseClickedHandler mouseClickedHandler = new TimeLineChart.MouseClickedHandler<>(this); + TimeLineChart.ChartDragHandler chartDragHandler = new TimeLineChart.ChartDragHandler<>(this); + configureMouseListeners(chart, mouseClickedHandler, chartDragHandler); + configureMouseListeners(pinnedChart, mouseClickedHandler, chartDragHandler); + + } + + @Override + public void clearIntervalSelector() { + rootPane.getChildren().remove(intervalSelector); + intervalSelector = null; + } + + @Override + public IntervalSelector newIntervalSelector() { + return new DetailIntervalSelector(this); + } + + @Override + public IntervalSelector getIntervalSelector() { + return intervalSelector; + } + + @Override + public void setIntervalSelector(IntervalSelector newIntervalSelector) { + intervalSelector = newIntervalSelector; + rootPane.getChildren().add(getIntervalSelector()); } public void setSelectionModel(MultipleSelectionModel> selectionModel) { @@ -188,7 +283,7 @@ public class DetailViewPane extends AbstractVisualizationPane getXAxis() { + public Axis getXAxis() { return detailsChartDateAxis; } @@ -215,6 +310,57 @@ public class DetailViewPane extends AbstractVisualizationPane c1, Boolean selected) { c1.applySelectionEffect(selected); + + } + + DateTime getDateTimeForPosition(double layoutX) { + return chart.getDateTimeForPosition(layoutX); + } + + void clearGuideLine(GuideLine guideLine) { + rootPane.getChildren().remove(guideLine); + guideLine = null; + } + + /** + * + * @param chartLane the value of chartLane + * @param mouseClickedHandler the value of mouseClickedHandler + * @param chartDragHandler1 the value of chartDragHandler1 + */ + private void configureMouseListeners(final DetailsChartLane chartLane, final TimeLineChart.MouseClickedHandler mouseClickedHandler, final TimeLineChart.ChartDragHandler chartDragHandler) { + chartLane.setOnMousePressed(chartDragHandler); + chartLane.setOnMouseReleased(chartDragHandler); + chartLane.setOnMouseDragged(chartDragHandler); + chartLane.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedHandler); + } + private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); //NON-NLS + + class PlaceMarkerAction extends Action { + + private GuideLine guideLine; + + @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(DetailViewPane.this); + guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); + addGuideLine(guideLine); + + } else { + guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); + } + }); + } + + } + + private void addGuideLine(GuideLine guideLine) { + rootPane.getChildren().add(guideLine); } private class DetailViewSettingsPane extends HBox { @@ -331,7 +477,7 @@ public class DetailViewPane extends AbstractVisualizationPane { + pinnedEventsToggle.selectedProperty().addListener(observable -> { boolean selected = pinnedEventsToggle.isSelected(); if (selected == false) { dividerPosition = masterDetailPane.getDividerPosition(); @@ -344,7 +490,16 @@ public class DetailViewPane extends AbstractVisualizationPane change) -> { + if (change.getSet().size() == 0) { + pinnedEventsToggle.setSelected(false); + } else { + pinnedEventsToggle.setSelected(true); + } + }); } + } public Action newUnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) { @@ -353,6 +508,7 @@ public class DetailViewPane extends AbstractVisualizationPane { - - static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS - static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS - - public void requestTimelineChartLayout(); - - public Node asNode(); - - public ObservableList getEventStripes(); - - public ObservableList> getSelectedNodes(); - - double layoutEventBundleNodes(final Collection> nodes, final double minY); - - ReadOnlyDoubleProperty maxVScrollProperty(); - - void setVScroll(double vScrollValue); - - @NbBundle.Messages({"HideDescriptionAction.displayName=Hide", - "HideDescriptionAction.displayMsg=Hide this group from the details view."}) - static class HideDescriptionAction extends Action { - - 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.requestTimelineChartLayout()); - chart.getController().getQuickHideFilters().add(testFilter); - return testFilter; - }); - descriptionFilter.setSelected(true); - }); - } - } - - @NbBundle.Messages({"UnhideDescriptionAction.displayName=Unhide"}) - static class UnhideDescriptionAction extends Action { - - 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)) - ); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java new file mode 100644 index 0000000000..2afddc0f77 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java @@ -0,0 +1,456 @@ +/* + * 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.detailview; + +import com.google.common.collect.Range; +import com.google.common.collect.TreeRangeMap; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +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.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.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.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 static javafx.scene.layout.Region.USE_PREF_SIZE; +import org.controlsfx.control.action.Action; +import org.joda.time.DateTime; +import org.openide.util.NbBundle; +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.FilteredEventsModel; +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.IntervalSelector; +import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; + +/** + * + * 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. + */ +abstract class DetailsChartLane extends XYChart implements TimeLineChart { + + private static final String STYLE_SHEET = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS + + static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS + static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS + + static final int MINIMUM_EVENT_NODE_GAP = 4; + static final int MINIMUM_ROW_HEIGHT = 24; + + private final DetailViewPane parentPane; + final TimeLineController controller; + final FilteredEventsModel filteredEvents; + final DetailViewLayoutSettings layoutSettings; + final ObservableList> selectedNodes; + + final Map> eventMap = new HashMap<>(); + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + final ObservableList events = FXCollections.observableArrayList(); + 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 Set activeQuickHidefilters; + + public boolean quickHideFiltersEnabled() { + return useQuickHideFilters; + } + + @Override + protected void layoutPlotChildren() { + setCursor(Cursor.WAIT); + maxY.set(0); + 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()) { + sortedNodes.stream() + .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() + .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); + } else { + maxY.set(layoutEventBundleNodes(sortedNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); + } + doAdditionalLayout(); + setCursor(null); + } + + @Override + public TimeLineController getController() { + return controller; + } + + public ObservableList> 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 + */ + final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); + + DetailsChartLane(DetailViewPane parentPane, Axis dateAxis, Axis verticalAxis, boolean useQuickHideFilters) { + super(dateAxis, verticalAxis); + this.parentPane = parentPane; + this.layoutSettings = parentPane.getLayoutSettings(); + this.controller = parentPane.getController(); + this.selectedNodes = parentPane.getSelectedNodes(); + this.filteredEvents = controller.getEventsModel(); + this.useQuickHideFilters = useQuickHideFilters; + + 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); + + 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> nodes, final double minY) { + // map from y-ranges to maximum x + TreeRangeMap 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) { + //is the node hiden by a quick hide filter? + + boolean quickHide = useQuickHideFilters && 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 = (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); + + if ((xLeft != bundleNode.getLayoutX()) || (yTop != bundleNode.getLayoutY())) { + //animate node to new position + bundleNode.animateTo(xLeft, yTop); + } + } + } + return localMax; //return new max + } + + final public void requestTimelineChartLayout() { + requestChartLayout(); + } + + double getXForEpochMillis(Long millis) { + DateTime dateTime = new DateTime(millis); + return getXAxis().getDisplayPosition(dateTime); + } + + abstract public ObservableList getEventStripes(); + + @Override + public ContextMenu getContextMenu() { + return parentPane.getContextMenu(); + } + + @Override + protected void dataItemAdded(Series series, int itemIndex, Data item) { + } + + @Override + protected void dataItemRemoved(Data item, Series series) { + } + + @Override + protected void dataItemChanged(Data item) { + } + + @Override + protected void seriesAdded(Series series, int seriesIndex) { + } + + @Override + protected void seriesRemoved(Series series) { + } + + @Override + public IntervalSelector getIntervalSelector() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void setIntervalSelector(IntervalSelector newIntervalSelector) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public IntervalSelector newIntervalSelector() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void clearIntervalSelector() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + /** + * 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 getXAxis().getValueForDisplay(getXAxis().parentToLocal(x, 0).getX()); + } + + @Override + public ContextMenu getChartContextMenu(MouseEvent clickEvent) { + return parentPane.getChartContextMenu(clickEvent); + } + /** + * 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> getNodes(Predicate> p) { + //use this recursive function to flatten the tree of nodes into an single stream. + Function, Stream>> stripeFlattener = + new Function, Stream>>() { + @Override + public Stream> 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()); + } + @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass + double descriptionWidth; + + /** + * 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 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 eventNode the Node to layout + * @param descriptionWdith the maximum width for the description text + */ + 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(); + + @NbBundle.Messages({"HideDescriptionAction.displayName=Hide", + "HideDescriptionAction.displayMsg=Hide this group from the details view."}) + static class HideDescriptionAction extends Action { + + HideDescriptionAction(String description, DescriptionLoD descriptionLoD, DetailsChartLane 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.requestTimelineChartLayout()); + chart.getController().getQuickHideFilters().add(testFilter); + return testFilter; + }); + descriptionFilter.setSelected(true); + }); + } + } + + @NbBundle.Messages({"UnhideDescriptionAction.displayName=Unhide"}) + static class UnhideDescriptionAction extends Action { + + UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD, DetailsChartLane 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)) + ); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java index d68f390894..40b2309d3a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java @@ -30,6 +30,10 @@ import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; */ class EventAxis extends Axis { + EventAxis(String label) { + setLabel(label); + } + @Override public double getDisplayPosition(Type value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index 5bb1e86c43..0be313272c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -89,8 +89,8 @@ final public class EventClusterNode extends MultiEventNodeBase chartLane, EventCluster eventCluster, EventStripeNode parentNode) { + super(chartLane, eventCluster, parentNode); subNodePane.setBorder(clusterBorder); subNodePane.setBackground(defaultBackground); @@ -146,7 +146,7 @@ final public class EventClusterNode extends MultiEventNodeBase createChildNode(EventStripe stripe) { if (stripe.getEventIDs().size() == 1) { - return new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(stripe.getEventIDs())), this); + return new SingleEventNode(getChartLane(), getChartLane().getController().getEventsModel().getEventById(Iterables.getOnlyElement(stripe.getEventIDs())), this); } else { - return new EventStripeNode(getChart(), stripe, this); + return new EventStripeNode(getChartLane(), stripe, this); } } @@ -231,8 +231,8 @@ final public class EventClusterNode extends MultiEventNodeBase sleuthkit 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.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.collections.FXCollections; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.geometry.Insets; -import javafx.scene.Cursor; -import javafx.scene.Group; -import javafx.scene.Node; -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; - -/** - * 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 implements DetailsChart { - - private static final String styleSheet = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //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 activeQuickHidefilters; - @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass - private double descriptionWidth; - private final DetailViewLayoutSettings layoutSettings; - - @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 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> 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 eventStripes = FXCollections.observableArrayList(); - private final ObservableList< EventNodeBase> stripeNodes = FXCollections.observableArrayList(); - private final ObservableList< EventNodeBase> sortedStripeNodes = stripeNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)); - private final Map projectionMap = new ConcurrentHashMap<>(); - - EventDetailsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes, DetailViewLayoutSettings layoutSettings) { - super(dateAxis, verticalAxis); - this.layoutSettings = layoutSettings; - - 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 - layoutSettings.bandByTypeProperty().addListener(layoutInvalidationListener); - layoutSettings.oneEventPerRowProperty().addListener(layoutInvalidationListener); - layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); - layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener); - layoutSettings.descrVisibilityProperty().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 chartDragHandler = new ChartDragHandler<>(this); - setOnMousePressed(chartDragHandler); - setOnMouseReleased(chartDragHandler); - setOnMouseDragged(chartDragHandler); - - setOnMouseClicked(new MouseClickedHandler<>(this)); - - this.selectedNodes = selectedNodes; - this.selectedNodes.addListener(new SelectionChangeHandler()); - } - - @Override - public void requestTimelineChartLayout() { - requestChartLayout(); - } - - public ObservableList> getSelectedNodes() { - return selectedNodes; - } - - @Override - public Node asNode() { - return this; - } - - public ObservableList 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; - } - - @Override - public IntervalSelector newIntervalSelector() { - return new DetailIntervalSelector(this); - } - - /** - * 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 getIntervalSelector() { - return intervalSelector; - } - - @Override - public void setIntervalSelector(IntervalSelector newIntervalSelector) { - intervalSelector = newIntervalSelector; - getChartChildren().add(getIntervalSelector()); - } - - /** - * @see note in main section of class JavaDoc - * - * @param series - * @param i - */ - @Override - protected void seriesAdded(Series series, int i) { - - } - - /** - * @see note in main section of class JavaDoc - * - * @param series - */ - @Override - protected void seriesRemoved(Series series) { - - } - - /** - * @see note in main section of class JavaDoc - * - * @param series - * @param itemIndex - * @param item - */ - @Override - protected void dataItemAdded(Series series, int itemIndex, Data item) { - } - - /** - * @see note in main section of class JavaDoc - * - * - * @param item - * @param series - */ - @Override - protected void dataItemRemoved(Data item, Series series) { - } - - /** - * @see note in main section of class JavaDoc - * - * @param item - */ - @Override - protected void dataItemChanged(Data item) { - } - - /** - * add a dataitem to this chart - * - * @see note in main section of class JavaDoc - * - * @param data - */ - void addDataItem(Data data) { - final EventStripe eventStripe = data.getYValue(); - EventNodeBase newNode; - if (eventStripe.getEventIDs().size() == 1) { - newNode = new SingleEventNode(this, controller.getEventsModel().getEventById(Iterables.getOnlyElement(eventStripe.getEventIDs())), null); - } else { - newNode = new EventStripeNode(EventDetailsChart.this, eventStripe, null); - } - Platform.runLater(() -> { - eventStripes.add(eventStripe); - stripeNodes.add(newNode); - nodeGroup.getChildren().add(newNode); - data.setNode(newNode); - }); - } - - /** - * remove a data item from this chart - * - * @see note in main section of class JavaDoc - * - * @param data - */ - void removeDataItem(Data data) { - Platform.runLater(() -> { - EventNodeBase removedNode = (EventNodeBase) 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 = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE; - - if (layoutSettings.getBandByType()) { - sortedStripeNodes.stream() - .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() - .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); - } else { - maxY.set(layoutEventBundleNodes(sortedStripeNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); - } - layoutProjectionMap(); - setCursor(null); - } - - public ReadOnlyDoubleProperty maxVScrollProperty() { - return maxY.getReadOnlyProperty(); - } - - /** - * @return all the nodes that pass the given predicate - */ - synchronized Iterable> getNodes(Predicate> p) { - //use this recursive function to flatten the tree of nodes into an single stream. - Function, Stream>> stripeFlattener = - new Function, Stream>>() { - @Override - public Stream> apply(EventNodeBase node) { - return Stream.concat( - Stream.of(node), - node.getSubNodes().stream().flatMap(this::apply)); - } - }; - - return sortedStripeNodes.stream() - .flatMap(stripeFlattener) - .filter(p).collect(Collectors.toList()); - } - - public 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. - */ - public double layoutEventBundleNodes(final Collection> nodes, final double minY) { - // map from y-ranges to maximum x - TreeRangeMap 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) { - //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 = (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); - - 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 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 eventNode the Node to layout - * @param descriptionWdith the maximum width for the description text - */ - private 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(); - } - - /** - * 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 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 { - - 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> { - - private final Axis dateAxis; - - SelectionChangeHandler() { - dateAxis = getXAxis(); - } - - @Override - public void onChanged(ListChangeListener.Change> change) { - while (change.next()) { - change.getRemoved().forEach((EventNodeBase removedNode) -> { - removedNode.getEvent().getClusters().forEach(cluster -> { - Line removedLine = projectionMap.remove(cluster); - getChartChildren().removeAll(removedLine); - }); - - }); - change.getAddedSubList().forEach((EventNodeBase addedNode) -> { - - for (EventCluster range : addedNode.getEvent().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); - } - }); - } - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java index ef93dba265..6fda53d6df 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -71,7 +71,6 @@ 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.ui.AbstractVisualizationPane; -import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; 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.DescriptionLoD; @@ -83,13 +82,14 @@ import org.sleuthkit.datamodel.TskCoreException; * */ public abstract class EventNodeBase extends StackPane { + static final Map dropShadowMap = new ConcurrentHashMap<>(); - + private static final Logger LOGGER = Logger.getLogger(EventNodeBase.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 - + 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 @@ -98,59 +98,59 @@ public abstract class EventNodeBase extends StackPan b.setMaxSize(16, 16); b.setPrefSize(16, 16); } - + static void show(Node b, boolean show) { b.setVisible(show); b.setManaged(show); } - + final Type tlEvent; - + final EventNodeBase parentNode; - + final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); final SimpleObjectProperty descVisibility = new SimpleObjectProperty<>(); - - final DetailsChart chart; + + 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_PIN); final ImageView tagIV = new ImageView(TAG); - + final HBox controlsHBox = new HBox(5); final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV, controlsHBox); - + final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); - + final ImageView eventTypeImageView = new ImageView(); final SleuthkitCase sleuthkitCase; final FilteredEventsModel eventsModel; private Timeline timeline; private Button pinButton; private final Border SELECTION_BORDER; - - EventNodeBase(Type ievent, EventNodeBase parent, DetailsChart chart) { - this.chart = chart; + + EventNodeBase(Type ievent, EventNodeBase parent, DetailsChartLane chart) { + this.chartLane = chart; this.tlEvent = ievent; this.parentNode = parent; - + sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); eventsModel = chart.getController().getEventsModel(); eventTypeImageView.setImage(getEventType().getFXImage()); - + descrLabel.setGraphic(eventTypeImageView); - + if (chart.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)); descVisibility.addListener(observable -> setDescriptionVisibiltiyImpl(descVisibility.get())); @@ -159,7 +159,7 @@ public abstract class EventNodeBase extends StackPan //set up mouse hover effect and tooltip setOnMouseEntered(mouseEntered -> { - Tooltip.uninstall(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); + Tooltip.uninstall(chart, AbstractVisualizationPane.getDefaultTooltip()); showHoverControls(true); toFront(); }); @@ -168,18 +168,20 @@ public abstract class EventNodeBase extends StackPan if (parentNode != null) { parentNode.showHoverControls(true); } else { - Tooltip.install(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); + Tooltip.install(chart, AbstractVisualizationPane.getDefaultTooltip()); } }); setOnMouseClicked(new ClickHandler()); show(controlsHBox, false); } - + public Type getEvent() { return tlEvent; } - - public abstract TimeLineChart getChart(); + + DetailsChartLane getChartLane() { + return chartLane; + } /** * @param w the maximum width the description label should have @@ -187,7 +189,7 @@ public abstract class EventNodeBase extends StackPan public void setMaxDescriptionWidth(double w) { descrLabel.setMaxWidth(w); } - + public abstract List> getSubNodes(); /** @@ -198,7 +200,7 @@ public abstract class EventNodeBase extends StackPan public void applySelectionEffect(boolean applied) { setBorder(applied ? SELECTION_BORDER : null); } - + protected void layoutChildren() { super.layoutChildren(); } @@ -215,7 +217,7 @@ public abstract class EventNodeBase extends StackPan configureActionButton(pinButton); } } - + final void showHoverControls(final boolean showControls) { Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(), eventType -> new DropShadow(-10, eventType.getColor())); @@ -223,9 +225,9 @@ public abstract class EventNodeBase extends StackPan installTooltip(); enableTooltip(showControls); installActionButtons(); - - TimeLineController controller = getChart().getController(); - + + TimeLineController controller = getChartLane().getController(); + if (controller.getPinnedEvents().contains(tlEvent)) { pinButton.setOnAction(actionEvent -> { new UnPinEventAction(controller, tlEvent).handle(actionEvent); @@ -239,7 +241,7 @@ public abstract class EventNodeBase extends StackPan }); pinButton.setGraphic(new ImageView(PIN)); } - + show(controlsHBox, showControls); if (parentNode != null) { parentNode.showHoverControls(false); @@ -268,7 +270,7 @@ public abstract class EventNodeBase extends StackPan { updateTitle(Bundle.EventNodeBase_toolTip_loading2()); } - + @Override protected String call() throws Exception { HashMap hashSetCounts = new HashMap<>(); @@ -288,7 +290,7 @@ public abstract class EventNodeBase extends StackPan String hashSetCountsString = hashSetCounts.entrySet().stream() .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) .collect(Collectors.joining("\n")); - + Map tagCounts = new HashMap<>(); if (tlEvent.getEventIDsWithTags().isEmpty() == false) { tagCounts.putAll(eventsModel.getTagCountsByTagName(tlEvent.getEventIDsWithTags())); @@ -296,14 +298,14 @@ public abstract class EventNodeBase extends StackPan String tagCountsString = tagCounts.entrySet().stream() .map((Map.Entry 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(); @@ -316,10 +318,10 @@ public abstract class EventNodeBase extends StackPan } }; new Thread(tooltTipTask).start(); - chart.getController().monitorTask(tooltTipTask); + chartLane.getController().monitorTask(tooltTipTask); } } - + void enableTooltip(boolean toolTipEnabled) { if (toolTipEnabled) { Tooltip.install(this, tooltip); @@ -327,27 +329,27 @@ public abstract class EventNodeBase extends StackPan 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 - ? getChart().getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis())) + ? getChartLane().getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis())) : 0; } - + abstract String getDescription(); - + void animateTo(double xLeft, double yTop) { if (timeline != null) { timeline.stop(); @@ -360,15 +362,15 @@ public abstract class EventNodeBase extends StackPan timeline.setOnFinished(finished -> Platform.runLater(this::requestChartLayout)); timeline.play(); } - + abstract void requestChartLayout(); - + void setDescriptionVisibility(DescriptionVisibility get) { descVisibility.set(get); } - + abstract void setDescriptionVisibiltiyImpl(DescriptionVisibility get); - + boolean hasDescription(String other) { return this.getDescription().startsWith(other); } @@ -387,23 +389,23 @@ public abstract class EventNodeBase extends StackPan setBackground(defaultBackground); } } - + void applyHighlightEffect() { applyHighlightEffect(true); } - + void clearHighlightEffect() { applyHighlightEffect(false); } - + abstract Collection getEventIDs(); - + abstract EventHandler getDoubleClickHandler(); - + abstract Collection getActions(); - + static class PinEventAction extends Action { - + @NbBundle.Messages({"PinEventAction.text=Pin"}) PinEventAction(TimeLineController controller, TimeLineEvent event) { super(Bundle.PinEventAction_text()); @@ -411,9 +413,9 @@ public abstract class EventNodeBase extends StackPan setGraphic(new ImageView(PIN)); } } - + static class UnPinEventAction extends Action { - + @NbBundle.Messages({"UnPinEventAction.text=Unpin"}) UnPinEventAction(TimeLineController controller, TimeLineEvent event) { super(Bundle.UnPinEventAction_text()); @@ -426,30 +428,30 @@ public abstract class EventNodeBase extends StackPan * event handler used for mouse events on {@link EventNodeBase}s */ class ClickHandler implements EventHandler { - + 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.getSelectedNodes().add(EventNodeBase.this); + chartLane.getSelectedNodes().add(EventNodeBase.this); } else if (t.isShortcutDown()) { - chart.getSelectedNodes().removeAll(EventNodeBase.this); + chartLane.getSelectedNodes().removeAll(EventNodeBase.this); } else { - chart.getSelectedNodes().setAll(EventNodeBase.this); + chartLane.getSelectedNodes().setAll(EventNodeBase.this); } t.consume(); } else if (t.getButton() == MouseButton.SECONDARY) { - ContextMenu chartContextMenu = chart.getChartContextMenu(t); + ContextMenu chartContextMenu = chartLane.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()); } @@ -457,7 +459,7 @@ public abstract class EventNodeBase extends StackPan t.consume(); } } - + } - + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 100be6b0d2..9485ab334b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -33,7 +33,7 @@ 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 org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChart.HideDescriptionAction; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChartLane.HideDescriptionAction; import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configureActionButton; /** @@ -44,14 +44,13 @@ final public class EventStripeNode extends MultiEventNodeBase chartLane, EventStripe eventStripe, EventClusterNode parentNode) { + super(chartLane, eventStripe, parentNode); setMinHeight(24); //setup description label - + descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); - + descrLabel.setPrefWidth(USE_COMPUTED_SIZE); setAlignment(subNodePane, Pos.BOTTOM_LEFT); @@ -64,10 +63,10 @@ final public class EventStripeNode extends MultiEventNodeBase childNode; EventCluster cluster = Iterables.getOnlyElement(eventStripe.getClusters()); if (cluster.getEventIDs().size() == 1) { - SingleEventNode singleEventNode = new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); + SingleEventNode singleEventNode = new SingleEventNode(getChartLane(), getChartLane().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); childNode = singleEventNode; } else { - EventClusterNode eventClusterNode = new EventClusterNode(getChart(), cluster, this); + EventClusterNode eventClusterNode = new EventClusterNode(getChartLane(), cluster, this); eventClusterNode.installActionButtons(); eventClusterNode.infoHBox.getChildren().remove(eventClusterNode.countLabel); controlsHBox.getChildren().addAll(eventClusterNode.minusButton, eventClusterNode.plusButton); @@ -84,13 +83,11 @@ final public class EventStripeNode extends MultiEventNodeBase createChildNode(EventCluster cluster) { if (cluster.getEventIDs().size() == 1) { - return new SingleEventNode(getChart(), getChart().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); + return new SingleEventNode(getChartLane(), getChartLane().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); } else { - return new EventClusterNode(getChart(), cluster, this); + return new EventClusterNode(getChartLane(), cluster, this); } } - - @Override void setDescriptionVisibiltiyImpl(DescriptionVisibility descrVis) { final int size = getEventStripe().getSize(); @@ -142,6 +137,6 @@ final public class EventStripeNode extends MultiEventNodeBase getActions() { - return Arrays.asList(new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chart)); + return Arrays.asList(new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chartLane)); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java index a462a92b9e..fe7db93425 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java @@ -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 DetailViewPane 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(DetailViewPane chart) { super(0, 0, 0, 0); this.chart = chart; Axis xAxis = chart.getXAxis(); @@ -65,7 +65,7 @@ class GuideLine extends Line { setOnMouseClicked(clickedEvent -> { if (clickedEvent.getButton() == MouseButton.SECONDARY && clickedEvent.isStillSincePress() == false) { - chart.clearGuideLine(); + chart.clearGuideLine(this); clickedEvent.consume(); } }); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java index 6e5a74710f..dc381ff142 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java @@ -66,7 +66,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent chart, BundleType eventBundle, ParentNodeType parentNode) { super(eventBundle, parentNode, chart); this.descLOD.set(eventBundle.getDescriptionLoD()); @@ -101,7 +101,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent { - Tooltip.uninstall(chart.asNode(), AbstractVisualizationPane.getDefaultTooltip()); + Tooltip.uninstall(chart, AbstractVisualizationPane.getDefaultTooltip()); showHoverControls(true); toFront(); }); @@ -110,7 +110,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent Platform.runLater(chart::requestTimelineChartLayout)); + timeline.setOnFinished(finished -> Platform.runLater(chartLane::requestTimelineChartLayout)); timeline.play(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java index fe4840b27b..e898ab11b4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java @@ -18,96 +18,21 @@ */ 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.Comparator; -import java.util.HashMap; -import java.util.Map; -import java.util.MissingResourceException; -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.collections.SetChangeListener; -import javafx.geometry.Insets; -import javafx.scene.Cursor; -import javafx.scene.Group; -import javafx.scene.Node; -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.controlsfx.control.action.ActionUtils; 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.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; -import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; -import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; -import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; /** * */ -public final class PinnedEventsChart extends XYChart implements DetailsChart { - - private static final String styleSheet = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS - private static final int MINIMUM_EVENT_NODE_GAP = 4; - private static final int MINIMUM_ROW_HEIGHT = 24; - - private static EventNodeBase createNode(PinnedEventsChart chart, TimeLineEvent event) { - 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); - } - } - private Map> eventMap = new HashMap<>(); - private ContextMenu chartContextMenu; - - private final TimeLineController controller; - private final FilteredEventsModel filteredEvents; - /** - * 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 events = FXCollections.observableArrayList(); - private final ObservableList< EventNodeBase> eventNodes = FXCollections.observableArrayList(); - private final ObservableList< EventNodeBase> sortedEventNodes = eventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)); - private double descriptionWidth; - - /** - * the maximum y value used so far during the most recent layout pass - */ - private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); - private final ObservableList> selectedNodes; - private final DetailViewLayoutSettings layoutSettings; - /** - * listener that triggers chart layout pass - */ - private final InvalidationListener layoutInvalidationListener = (Observable o) -> { - layoutPlotChildren(); - }; +public final class PinnedEventsChart extends DetailsChartLane { /** * @@ -116,58 +41,19 @@ public final class PinnedEventsChart extends XYChart im * @param verticalAxis the value of verticalAxis * @param selectedNodes1 the value of selectedNodes1 */ - PinnedEventsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes, DetailViewLayoutSettings layoutSettings) { - super(dateAxis, verticalAxis); - this.layoutSettings = layoutSettings; - this.controller = controller; - this.filteredEvents = this.controller.getEventsModel(); + PinnedEventsChart(DetailViewPane parentPane, DateAxis dateAxis, final Axis verticalAxis) { + super(parentPane, dateAxis, verticalAxis, false); - sceneProperty().addListener(observable -> { - Scene scene = getScene(); - if (scene != null && scene.getStylesheets().contains(styleSheet) == false) { - scene.getStylesheets().add(styleSheet); - } - }); - - filteredEvents.zoomParametersProperty().addListener(o -> { - }); - - 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); final Series series = new Series<>(); setData(FXCollections.observableArrayList()); getData().add(series); -// //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); - getController().getQuickHideFilters().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 chartDragHandler = new ChartDragHandler<>(this); -// setOnMousePressed(chartDragHandler); -// setOnMouseReleased(chartDragHandler); -// setOnMouseDragged(chartDragHandler); -// -// setOnMouseClicked(new MouseClickedHandler<>(this)); - controller.getPinnedEvents().addListener((SetChangeListener.Change change) -> { + getController().getPinnedEvents().addListener((SetChangeListener.Change change) -> { if (change.wasAdded()) { TimeLineEvent elementAdded = change.getElementAdded(); Data data1 = new Data<>(new DateTime(elementAdded.getStartMillis()), elementAdded); @@ -183,30 +69,7 @@ public final class PinnedEventsChart extends XYChart im requestChartLayout(); }); - this.selectedNodes = selectedNodes; - } - @Override - public ContextMenu getChartContextMenu() { - return chartContextMenu; - } - - @Override - public ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException { - if (chartContextMenu != null) { - chartContextMenu.hide(); - } - - chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(//new EventDetailsChart.PlaceMarkerAction(clickEvent), - TimeLineChart.newZoomHistoyActionGroup(controller))); - chartContextMenu.setAutoHide(true); - return chartContextMenu; - } -// final ObservableList> selectedNodes; - - @Override - public Node asNode() { - return this; } @Override @@ -214,136 +77,63 @@ public final class PinnedEventsChart extends XYChart im return FXCollections.emptyObservableList(); } - @Override - public IntervalSelector getIntervalSelector() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public void setIntervalSelector(IntervalSelector newIntervalSelector) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public IntervalSelector newIntervalSelector() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public void clearIntervalSelector() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public TimeLineController getController() { - return controller; - } - - @Override - public void requestTimelineChartLayout() { - requestChartLayout(); - } - - @Override - public ObservableList> getSelectedNodes() { - return selectedNodes; - } - - @Override - public double layoutEventBundleNodes(final Collection> nodes, final double minY) { - // map from y-ranges to maximum x - TreeRangeMap 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 eventNode : nodes) { - //is the node hiden by a quick hide filter? - - layoutBundleHelper(eventNode); - //get computed height and width - double h = eventNode.getBoundsInLocal().getHeight(); - double w = eventNode.getBoundsInLocal().getWidth(); - //get left and right x coords from axis plus computed width - double xLeft = getXForEpochMillis(eventNode.getStartMillis()) - eventNode.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); - - if ((xLeft != eventNode.getLayoutX()) || (yTop != eventNode.getLayoutY())) { - //animate node to new position - eventNode.animateTo(xLeft, yTop); - } - - } - return localMax; //return new max - } - - @Override - protected void dataItemAdded(Series series, int itemIndex, Data item) { - } - - @Override - protected void dataItemRemoved(Data item, Series series) { - } - - @Override - protected void dataItemChanged(Data item) { - } - - @Override - protected void seriesAdded(Series series, int seriesIndex) { - } - - @Override - protected void seriesRemoved(Series series) { - } - - @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 = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE; - - if (layoutSettings.getBandByType()) { - sortedEventNodes.stream() - .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() - .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); - } else { - maxY.set(layoutEventBundleNodes(sortedEventNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); - } - setCursor(null); - } - - /** - * expose as protected - */ - @Override - protected void requestChartLayout() { - super.requestChartLayout(); - } - - public synchronized void setVScroll(double vScrollValue) { - nodeGroup.setTranslateY(-vScrollValue); - } - - public ReadOnlyDoubleProperty maxVScrollProperty() { - return maxY.getReadOnlyProperty(); - } - +// @Override +// public double layoutEventBundleNodes(final Collection> nodes, final double minY) { +// // map from y-ranges to maximum x +// TreeRangeMap 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 eventNode : nodes) { +// //is the node hiden by a quick hide filter? +// +// layoutBundleHelper(eventNode); +// //get computed height and width +// double h = eventNode.getBoundsInLocal().getHeight(); +// double w = eventNode.getBoundsInLocal().getWidth(); +// //get left and right x coords from axis plus computed width +// double xLeft = getXForEpochMillis(eventNode.getStartMillis()) - eventNode.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); +// +// if ((xLeft != eventNode.getLayoutX()) || (yTop != eventNode.getLayoutY())) { +// //animate node to new position +// eventNode.animateTo(xLeft, yTop); +// } +// +// } +// return localMax; //return new max +// } +// @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 = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE; +// +// if (layoutSettings.getBandByType()) { +// sortedNodes.stream() +// .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() +// .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); +// } else { +// maxY.set(layoutEventBundleNodes(sortedNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); +// } +// setCursor(null); +// } /** * add a dataitem to this chart * @@ -358,31 +148,21 @@ public final class PinnedEventsChart extends XYChart im eventMap.put(event, eventNode); Platform.runLater(() -> { events.add(event); - eventNodes.add(eventNode); + nodes.add(eventNode); nodeGroup.getChildren().add(eventNode); data.setNode(eventNode); }); } - /** - * @return all the nodes that pass the given predicate - */ - synchronized Iterable> getNodes(Predicate> p) { - //use this recursive function to flatten the tree of nodes into an single stream. - Function, Stream>> stripeFlattener = - new Function, Stream>>() { - @Override - public Stream> apply(EventNodeBase node) { - return Stream.concat( - Stream.of(node), - node.getSubNodes().stream().flatMap(this::apply)); - } - }; - - return sortedEventNodes.stream() - .flatMap(stripeFlattener) - .filter(p).collect(Collectors.toList()); + static EventNodeBase createNode(DetailsChartLane chart, TimeLineEvent event) { + 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); + } } /** @@ -396,50 +176,14 @@ public final class PinnedEventsChart extends XYChart im EventNodeBase removedNode = eventMap.remove(data.getYValue()); Platform.runLater(() -> { events.removeAll(data.getYValue()); - eventNodes.removeAll(removedNode); + nodes.removeAll(removedNode); nodeGroup.getChildren().removeAll(removedNode); data.setNode(null); }); } - private double getXForEpochMillis(Long millis) { - DateTime dateTime = new DateTime(millis); - return getXAxis().getDisplayPosition(dateTime); + @Override + void doAdditionalLayout() { } - private double computeYTop(double yMin, double h, TreeRangeMap 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; - } - - private 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(); - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java new file mode 100644 index 0000000000..06a871ffd1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java @@ -0,0 +1,163 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-15 Basis Technology Corp. + * Contact: carrier sleuthkit 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 java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import javafx.application.Platform; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +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.joda.time.DateTime; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; + +/** + * 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 PrimaryDetailsChart extends DetailsChartLane { + + 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 projectionMap = new ConcurrentHashMap<>(); + + PrimaryDetailsChart(DetailViewPane parentPane, DateAxis dateAxis, final Axis verticalAxis) { + super(parentPane, dateAxis, verticalAxis, true); + +// filteredEvents.zoomParametersProperty().addListener(o -> { +// selectedNodes.clear(); +// projectionMap.clear(); +// controller.selectEventIDs(Collections.emptyList()); +// }); +// //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()); +// }); + //add listener for events that should trigger layout + getController().getQuickHideFilters().addListener(layoutInvalidationListener); + + selectedNodes.addListener((ListChangeListener.Change> 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); + } + }); + } + }); + } + + public ObservableList getEventStripes() { + return events; + } + + /** + * add a dataitem to this chart + * + * @see note in main section of class JavaDoc + * + * @param data + */ + void addDataItem(Data data) { + final EventStripe eventStripe = data.getYValue(); + EventNodeBase newNode; + if (eventStripe.getEventIDs().size() == 1) { + newNode = new SingleEventNode(this, controller.getEventsModel().getEventById(Iterables.getOnlyElement(eventStripe.getEventIDs())), null); + } else { + newNode = new EventStripeNode(PrimaryDetailsChart.this, eventStripe, null); + } + Platform.runLater(() -> { + events.add(eventStripe); + nodes.add(newNode); + nodeGroup.getChildren().add(newNode); + data.setNode(newNode); + }); + } + + /** + * remove a data item from this chart + * + * @see note in main section of class JavaDoc + * + * @param data + */ + void removeDataItem(Data data) { + Platform.runLater(() -> { + EventNodeBase removedNode = (EventNodeBase) data.getNode(); + events.removeAll(new StripeFlattener().apply(removedNode).collect(Collectors.toList())); + nodes.removeAll(removedNode); + nodeGroup.getChildren().removeAll(removedNode); + data.setNode(null); + }); + } + + private double getParentXForEpochMillis(Long epochMillis) { + return getXAxis().localToParent(getXForEpochMillis(epochMillis), 0).getX(); + } + + void doAdditionalLayout() { + for (final Map.Entry 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); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingWrapper.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingLaneWrapper.java similarity index 79% rename from Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingWrapper.java rename to Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingLaneWrapper.java index 11ca4fe780..67d569351b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingWrapper.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingLaneWrapper.java @@ -6,35 +6,31 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import javafx.application.Platform; -import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.geometry.Orientation; -import javafx.scene.chart.XYChart; 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; -import org.joda.time.DateTime; -import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; /** * */ -public class ScrollingWrapper & DetailsChart> extends BorderPane { +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 ChartType chart; + private final DetailsChartLane chart; - public ScrollingWrapper(ChartType center) { + ScrollingLaneWrapper(DetailsChartLane center) { super(center); this.chart = center; - scrollBarSpacer.minHeightProperty().bind(((XYChart) chart).getXAxis().heightProperty()); + scrollBarSpacer.minHeightProperty().bind(chart.getXAxis().heightProperty()); //configure scrollbar vertScrollBar.setOrientation(Orientation.VERTICAL); @@ -45,16 +41,13 @@ public class ScrollingWrapper { + chart.setVScroll(vertScrollBar.getValue()); }); //request focus for keyboard scrolling setOnMouseClicked(mouseEvent -> requestFocus()); -//interpret scroll events to the scrollBar + //interpret scroll events to the scrollBar this.setOnScroll(scrollEvent -> vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() - scrollEvent.getDeltaY()))); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java index daf89d9a9c..cb7bafd1ce 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java @@ -35,11 +35,9 @@ import javafx.scene.layout.CornerRadii; import static javafx.scene.layout.Region.USE_PREF_SIZE; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.action.Action; -import org.joda.time.DateTime; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; -import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; /** * @@ -62,7 +60,7 @@ final class SingleEventNode extends EventNodeBase { @Override Collection getActions() { - TimeLineController controller = getChart().getController(); + TimeLineController controller = getChartLane().getController(); if (controller.getPinnedEvents().contains(tlEvent)) { return Arrays.asList(new UnPinEventAction(controller, tlEvent)); } else { @@ -70,7 +68,7 @@ final class SingleEventNode extends EventNodeBase { } } - SingleEventNode(DetailsChart chart, SingleEvent event, MultiEventNodeBase parent) { + SingleEventNode(DetailsChartLane chart, SingleEvent event, MultiEventNodeBase parent) { super(event, parent, chart); this.descrLabel.setText(event.getFullDescription()); eventTypeImageView.setImage(getEventType().getFXImage()); @@ -94,10 +92,6 @@ final class SingleEventNode extends EventNodeBase { getChildren().add(infoHBox); } - @Override - public TimeLineChart getChart() { - return chart; - } @Override public List> getSubNodes() { @@ -117,7 +111,7 @@ final class SingleEventNode extends EventNodeBase { @Override void requestChartLayout() { - chart.requestTimelineChartLayout(); + chartLane.requestTimelineChartLayout(); } From 55f0ae10f32dce212c0a0c846ac623adf266ce6c Mon Sep 17 00:00:00 2001 From: jmillman Date: Fri, 4 Mar 2016 14:02:03 -0500 Subject: [PATCH 14/35] general cleanup; populate pinned events on creation of lane --- .../timeline/ui/detailview/DateAxis.java | 62 ++++++++-------- .../ui/detailview/DetailViewPane.java | 20 ++--- .../ui/detailview/DetailsChartLane.java | 15 +++- .../ui/detailview/PinnedEventsChart.java | 74 ++----------------- 4 files changed, 61 insertions(+), 110 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java index df1244596e..d30be7fe28 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java @@ -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; @@ -50,8 +50,8 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; * @author Diego Cirujano */ final class DateAxis extends Axis { - - private ObjectProperty lowerBound = new ObjectPropertyBase() { + + private ObjectProperty lowerBound = new ObjectPropertyBase(new DateTime(0)) { @Override protected void invalidated() { if (!isAutoRanging()) { @@ -59,12 +59,12 @@ final class DateAxis extends Axis { requestAxisLayout(); } } - + @Override public Object getBean() { return DateAxis.this; } - + @Override public String getName() { return "lowerBound"; // NON-NLS @@ -84,12 +84,12 @@ final class DateAxis extends Axis { * bounds. */ private DateTime minDate; - + private RangeDivisionInfo rangeDivisionInfo; - + private final ReadOnlyDoubleWrapper tickSpacing = new ReadOnlyDoubleWrapper(); - - private final ObjectProperty upperBound = new ObjectPropertyBase() { + + private final ObjectProperty upperBound = new ObjectPropertyBase(new DateTime(1)) { @Override protected void invalidated() { if (!isAutoRanging()) { @@ -97,12 +97,12 @@ final class DateAxis extends Axis { requestAxisLayout(); } } - + @Override public Object getBean() { return DateAxis.this; } - + @Override public String getName() { return "upperBound"; // NON-NLS @@ -120,7 +120,7 @@ final class DateAxis extends Axis { setTickLength(0); setTickMarkVisible(false); } - + @Override public double getDisplayPosition(DateTime date) { final double length = -200 + (getSide().isHorizontal() ? getWidth() : getHeight()); @@ -187,7 +187,7 @@ final class DateAxis extends Axis { public void setUpperBound(DateTime date) { upperBound.set(date); } - + @Override public DateTime getValueForDisplay(double displayPosition) { final double length = - 200 + (getSide().isHorizontal() ? getWidth() : getHeight()); @@ -198,7 +198,7 @@ final class DateAxis extends Axis { // Get the actual range of the visible area. // The minimal date should start at the zero position, that's why we subtract it. double range = length - getZeroPosition(); - + if (getSide().isVertical()) { // displayPosition = getHeight() - ((date - lowerBound) / diff) * range + getZero // date = displayPosition - getZero - getHeight())/range * diff + lowerBound @@ -209,16 +209,16 @@ final class DateAxis extends Axis { return new DateTime((long) ((displayPosition - getZeroPosition()) / range * diff + getLowerBound().getMillis()), TimeLineController.getJodaTimeZone()); } } - + @Override public double getZeroPosition() { return 0; } - + @Override public void invalidateRange(List list) { super.invalidateRange(list); - + Collections.sort(list); if (list.isEmpty()) { minDate = maxDate = new DateTime(); @@ -229,22 +229,22 @@ final class DateAxis extends Axis { maxDate = list.get(list.size() - 1); } } - + @Override public boolean isValueOnAxis(DateTime date) { return date.getMillis() > getLowerBound().getMillis() && date.getMillis() < getUpperBound().getMillis(); } - + @Override public double toNumericValue(DateTime date) { return date.getMillis(); } - + @Override public DateTime toRealValue(double v) { return new DateTime((long) v); } - + @Override protected Interval autoRange(double length) { if (isAutoRanging()) { @@ -256,7 +256,7 @@ final class DateAxis extends Axis { return getRange(); } } - + @Override protected List calculateTickValues(double length, Object range) { List tickDates = new ArrayList<>(); @@ -266,13 +266,13 @@ final class DateAxis extends Axis { rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo((Interval) range); final DateTime lowerBound1 = getLowerBound(); final DateTime upperBound1 = getUpperBound(); - + if (lowerBound1 == null || upperBound1 == null) { return tickDates; } DateTime lower = lowerBound1.withZone(TimeLineController.getJodaTimeZone()); DateTime upper = upperBound1.withZone(TimeLineController.getJodaTimeZone()); - + DateTime current = lower; // Loop as long we exceeded the upper bound. while (current.isBefore(upper)) { @@ -304,7 +304,7 @@ final class DateAxis extends Axis { tickDates.remove(lastDate); } } - + if (tickDates.size() >= 2) { tickSpacing.set(getDisplayPosition(tickDates.get(1)) - getDisplayPosition(tickDates.get(0))); } else if (tickDates.size() >= 4) { @@ -312,17 +312,17 @@ final class DateAxis extends Axis { } return tickDates; } - + @Override protected Interval getRange() { return new Interval(getLowerBound(), getUpperBound()); } - + @Override protected String getTickMarkLabel(DateTime date) { return rangeDivisionInfo.getTickFormatter().print(date); } - + @Override protected void layoutChildren() { super.layoutChildren(); @@ -339,7 +339,7 @@ final class DateAxis extends Axis { setLowerBound(new DateTime(rangeDivisionInfo.getLowerBound(), TimeLineController.getJodaTimeZone())); setUpperBound(new DateTime(rangeDivisionInfo.getUpperBound(), TimeLineController.getJodaTimeZone())); } - + ReadOnlyDoubleProperty getTickSpacing() { return tickSpacing.getReadOnlyProperty(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 31f1cc4ef1..d6b0796c3e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -168,6 +168,7 @@ public class DetailViewPane extends AbstractVisualizationPane change) -> { - if (change.getSet().size() == 0) { - pinnedEventsToggle.setSelected(false); - } else { - pinnedEventsToggle.setSelected(true); - } + boolean empty = change.getSet().isEmpty(); + pinnedEventsToggle.setSelected(empty == false); }); - } +// Platform.runLater(() -> { +// if (controller.getPinnedEvents().isEmpty() == false) { +// pinnedEventsToggle.setSelected(true); +// } +// }); + } } public Action newUnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java index 2afddc0f77..814752874c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java @@ -5,6 +5,7 @@ */ 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.Collection; @@ -40,8 +41,10 @@ import org.joda.time.DateTime; 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.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; @@ -87,7 +90,17 @@ abstract class DetailsChartLane extends XYChart 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 protected void layoutPlotChildren() { setCursor(Cursor.WAIT); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java index e898ab11b4..086dcfc5de 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java @@ -23,10 +23,9 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.SetChangeListener; import javafx.scene.chart.Axis; +import javafx.scene.chart.XYChart; import org.joda.time.DateTime; -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; /** @@ -70,6 +69,10 @@ public final class PinnedEventsChart extends DetailsChartLane { requestChartLayout(); }); + for (TimeLineEvent event : getController().getPinnedEvents()) { + addDataItem(new XYChart.Data<>(new DateTime(event.getStartMillis()), event)); + } + } @Override @@ -77,63 +80,6 @@ public final class PinnedEventsChart extends DetailsChartLane { return FXCollections.emptyObservableList(); } -// @Override -// public double layoutEventBundleNodes(final Collection> nodes, final double minY) { -// // map from y-ranges to maximum x -// TreeRangeMap 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 eventNode : nodes) { -// //is the node hiden by a quick hide filter? -// -// layoutBundleHelper(eventNode); -// //get computed height and width -// double h = eventNode.getBoundsInLocal().getHeight(); -// double w = eventNode.getBoundsInLocal().getWidth(); -// //get left and right x coords from axis plus computed width -// double xLeft = getXForEpochMillis(eventNode.getStartMillis()) - eventNode.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); -// -// if ((xLeft != eventNode.getLayoutX()) || (yTop != eventNode.getLayoutY())) { -// //animate node to new position -// eventNode.animateTo(xLeft, yTop); -// } -// -// } -// return localMax; //return new max -// } -// @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 = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE; -// -// if (layoutSettings.getBandByType()) { -// sortedNodes.stream() -// .collect(Collectors.groupingBy(EventNodeBase::getEventType)).values() -// .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); -// } else { -// maxY.set(layoutEventBundleNodes(sortedNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); -// } -// setCursor(null); -// } /** * add a dataitem to this chart * @@ -155,16 +101,6 @@ public final class PinnedEventsChart extends DetailsChartLane { }); } - static EventNodeBase createNode(DetailsChartLane chart, TimeLineEvent event) { - 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); - } - } - /** * remove a data item from this chart * From 69d1f2d026ebf2e9f1e0a6ef31c1e2a446835837 Mon Sep 17 00:00:00 2001 From: jmillman Date: Fri, 4 Mar 2016 16:25:32 -0500 Subject: [PATCH 15/35] open pinnedView on initialization if pinnedEvents is not empty DetailsChartLane doesn't implement timelinechart, only ContextMenuProvider Abstract DetailsChart as a Control out of DetailViewPane to encapuslate the Lanes, etc fixing mouse actions, moving things to appropriate levels in hierarchy, cleanup more cleanup, some features are still broken continue to cleanup DetailsChart API and fix bugs work on listeners to fix rightclick actions --- .../ui/AbstractVisualizationPane.java | 26 +- .../timeline/ui/ContextMenuProvider.java | 19 - .../autopsy/timeline/ui/IntervalSelector.java | 34 +- .../autopsy/timeline/ui/TimeLineChart.java | 38 +- .../timeline/ui/VisualizationPanel.java | 139 ++++--- .../ui/countsview/CountsViewPane.java | 19 +- .../ui/countsview/EventCountsChart.java | 7 +- .../ui/detailview/DetailViewPane.java | 303 +++----------- .../timeline/ui/detailview/DetailsChart.java | 390 ++++++++++++++++++ .../ui/detailview/DetailsChartLane.java | 170 ++++---- .../ui/detailview/EventClusterNode.java | 2 +- .../timeline/ui/detailview/EventNodeBase.java | 14 +- .../ui/detailview/EventStripeNode.java | 13 +- .../timeline/ui/detailview/GuideLine.java | 6 +- .../ui/detailview/MultiEventNodeBase.java | 18 +- .../ui/detailview/PinnedEventsChart.java | 125 ------ .../ui/detailview/PinnedEventsChartLane.java | 70 ++++ ...hart.java => PrimaryDetailsChartLane.java} | 65 +-- .../ui/detailview/SingleEventNode.java | 2 +- .../ui/detailview/tree/EventsTree.java | 14 +- .../netbeans/core/startup/Bundle.properties | 2 +- .../core/windows/view/ui/Bundle.properties | 2 +- 22 files changed, 794 insertions(+), 684 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChartLane.java rename Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/{PrimaryDetailsChart.java => PrimaryDetailsChartLane.java} (69%) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index 9c4bbfa1d7..1022a925b9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -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 & TimeLineChart> extends BorderPane { +public abstract class AbstractVisualizationPane> 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()); @@ -136,6 +134,9 @@ public abstract class AbstractVisualizationPane> getDataSeries() { +// return FXCollections.unmodifiableObservableList(dataSeries); +// } /** * @return the list of nodes containing settings widgets to insert into this * visualization's header @@ -199,6 +200,7 @@ public abstract class AbstractVisualizationPane getYAxis(); + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) abstract protected void resetData(); /** @@ -382,14 +384,11 @@ public abstract class AbstractVisualizationPane { - if (event.getButton() == MouseButton.PRIMARY && event.isStillSincePress()) { - selectedNodes.clear(); - } - }); - } - +// protected void setChartClickHandler() { +// chart.addEventHandler(MouseEvent.MOUSE_CLICKED, (MouseEvent event) -> { +// +// }); +// } /** * add a {@link Text} node to the leaf container for the decluttered axis * labels @@ -548,11 +547,12 @@ public abstract class AbstractVisualizationPane { + chart.clearIntervalSelector(); + resetData(); setDateAxisValues(axisValues); }); - } abstract protected void setDateAxisValues(AxisValuesType values); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java index b25d6f1972..70499f790e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java @@ -5,14 +5,12 @@ */ package org.sleuthkit.autopsy.timeline.ui; -import javafx.scene.chart.Axis; import javafx.scene.control.ContextMenu; import javafx.scene.input.MouseEvent; import org.sleuthkit.autopsy.timeline.TimeLineController; public interface ContextMenuProvider { - public Axis getXAxis(); public TimeLineController getController(); @@ -20,21 +18,4 @@ public interface ContextMenuProvider { ContextMenu getChartContextMenu(MouseEvent m); - IntervalSelector getIntervalSelector(); - - void setIntervalSelector(IntervalSelector newIntervalSelector); - - /** - * derived classes should implement this so as to supply an appropriate - * subclass of {@link IntervalSelector} - * - * @return a new interval selector - */ - IntervalSelector newIntervalSelector(); - - /** - * clear any references to previous interval selectors , including removing - * the interval selector from the ui / scene-graph - */ - void clearIntervalSelector(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java index c8d38ca6ec..62119aad26 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java @@ -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; @@ -61,7 +62,7 @@ public abstract class IntervalSelector extends BorderPane { /** * the Axis this is a selector over */ - public final ContextMenuProvider chart; + public final IntervalSelectorProvider chart; private Tooltip tooltip; /////////drag state @@ -89,7 +90,7 @@ public abstract class IntervalSelector extends BorderPane { @FXML private BorderPane bottomBorder; - public IntervalSelector(ContextMenuProvider chart) { + public IntervalSelector(IntervalSelectorProvider chart) { this.chart = chart; this.controller = chart.getController(); FXMLConstructor.construct(this, IntervalSelector.class, "IntervalSelector.fxml"); // NON-NLS @@ -191,13 +192,11 @@ public abstract class IntervalSelector extends BorderPane { 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) { chart.clearIntervalSelector(); mosueClick.consume(); - } - if (mosueClick.getClickCount() >= 2) { + } else if (mosueClick.getClickCount() >= 2) { zoomToSelectedInterval(); mosueClick.consume(); } @@ -313,4 +312,29 @@ public abstract class IntervalSelector extends BorderPane { }); } } + + public interface IntervalSelectorProvider { + + public TimeLineController getController(); + + IntervalSelector getIntervalSelector(); + + void setIntervalSelector(IntervalSelector newIntervalSelector); + + /** + * derived classes should implement this so as to supply an appropriate + * subclass of {@link IntervalSelector} + * + * @return a new interval selector + */ + IntervalSelector newIntervalSelector(); + + /** + * clear any references to previous interval selectors , including + * removing the interval selector from the ui / scene-graph + */ + void clearIntervalSelector(); + + public Axis getXAxis(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java index 05ff1911b3..c612decef3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java @@ -18,10 +18,13 @@ */ 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.control.ContextMenu; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Region; @@ -30,15 +33,17 @@ 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 the type of values along the horizontal axis */ -public interface TimeLineChart extends ContextMenuProvider { +public interface TimeLineChart extends ContextMenuProvider, IntervalSelectorProvider { + + ObservableList getSelectedNodes(); -// void setController(TimeLineController controller); IntervalSelector getIntervalSelector(); void setIntervalSelector(IntervalSelector newIntervalSelector); @@ -68,7 +73,7 @@ public interface TimeLineChart extends ContextMenuProvider { * @param the type of values along the horizontal axis * @param the type of chart this is a drag handler for */ - public static class ChartDragHandler> implements EventHandler { + public static class ChartDragHandler> implements EventHandler { private final Y chart; @@ -92,14 +97,14 @@ public interface TimeLineChart extends ContextMenuProvider { chart.getIntervalSelector().prefHeightProperty().bind(chart.heightProperty()); startX = mouseEvent.getX(); chart.getIntervalSelector().relocate(startX, 0); - } else //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()); - } + } else if (mouseEvent.getX() > startX) { + //resize/position existing selector + 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) { chart.setCursor(Cursor.DEFAULT); @@ -107,10 +112,9 @@ public interface TimeLineChart extends ContextMenuProvider { chart.setCursor(Cursor.DEFAULT); } } - } - static class MouseClickedHandler> implements EventHandler { + static class MouseClickedHandler> implements EventHandler { private final C chart; @@ -123,10 +127,12 @@ public interface TimeLineChart extends ContextMenuProvider { if (chart.getContextMenu() != null) { chart.getContextMenu().hide(); } - if (mouseEvent.getButton() == MouseButton.SECONDARY && mouseEvent.isStillSincePress()) { - chart.getChartContextMenu(mouseEvent); + if (mouseEvent.getButton() == MouseButton.PRIMARY && mouseEvent.isStillSincePress()) { + chart.getSelectedNodes().clear(); + } else if (MouseEvent.MOUSE_CLICKED == mouseEvent.getEventType() && mouseEvent.isPopupTrigger() && mouseEvent.isStillSincePress()) { + ContextMenu chartContextMenu = chart.getChartContextMenu(mouseEvent); chart.setOnMouseMoved(this); - chart.getContextMenu().show(chart, mouseEvent.getScreenX(), mouseEvent.getScreenY()); + chartContextMenu.show(chart, mouseEvent.getScreenX(), mouseEvent.getScreenY()); mouseEvent.consume(); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java index 2f56b78f79..bc7007d659 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java @@ -383,7 +383,10 @@ final public class VisualizationPanel extends BorderPane { notificationPane.setContent(visualization); if (visualization instanceof DetailViewPane) { - eventsTree.setDetailViewPane((DetailViewPane) visualization); + Platform.runLater(() -> { + 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); @@ -424,79 +427,79 @@ final public class VisualizationPanel extends BorderPane { histogramTask = new LoggedTask( NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.title"), true) { // NON-NLS - private final Lighting lighting = new Lighting(); + 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 + updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.preparing")); // NON-NLS - long max = 0; - final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()); - final long lowerBound = rangeInfo.getLowerBound(); - final long upperBound = rangeInfo.getUpperBound(); - Interval timeRange = new Interval(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()), new DateTime(upperBound, TimeLineController.getJodaTimeZone())); + long max = 0; + final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()); + final long lowerBound = rangeInfo.getLowerBound(); + final long upperBound = rangeInfo.getUpperBound(); + Interval timeRange = new Interval(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()), new DateTime(upperBound, TimeLineController.getJodaTimeZone())); - //extend range to block bounderies (ie day, month, year) - int p = 0; // progress counter + //extend range to block bounderies (ie day, month, year) + int p = 0; // progress counter - //clear old data, and reset ranges and series - Platform.runLater(() -> { - updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.resetUI")); // NON-NLS + //clear old data, and reset ranges and series + Platform.runLater(() -> { + updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.resetUI")); // NON-NLS - }); + }); - ArrayList bins = new ArrayList<>(); + ArrayList bins = new ArrayList<>(); - DateTime start = timeRange.getStart(); - while (timeRange.contains(start)) { - if (isCancelled()) { - return null; - } - DateTime end = start.plus(rangeInfo.getPeriodSize().getPeriod()); - final Interval interval = new Interval(start, end); - //increment for next iteration - - start = end; - - updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.queryDb")); // NON-NLS - //query for current range - long count = filteredEvents.getEventCounts(interval).values().stream().mapToLong(Long::valueOf).sum(); - bins.add(count); - - max = Math.max(count, max); - - final double fMax = Math.log(max); - final ArrayList fbins = new ArrayList<>(bins); - Platform.runLater(() -> { - updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.updateUI2")); // NON-NLS - - 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); - } - }); - } + DateTime start = timeRange.getStart(); + while (timeRange.contains(start)) { + if (isCancelled()) { return null; } + DateTime end = start.plus(rangeInfo.getPeriodSize().getPeriod()); + final Interval interval = new Interval(start, end); + //increment for next iteration - }; + start = end; + + updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.queryDb")); // NON-NLS + //query for current range + long count = filteredEvents.getEventCounts(interval).values().stream().mapToLong(Long::valueOf).sum(); + bins.add(count); + + max = Math.max(count, max); + + final double fMax = Math.log(max); + final ArrayList fbins = new ArrayList<>(bins); + Platform.runLater(() -> { + updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.updateUI2")); // NON-NLS + + 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); + } + }); + } + return null; + } + + }; new Thread(histogramTask).start(); controller.monitorTask(histogramTask); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java index e687412022..da2de36761 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java @@ -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 s : dataSeries) { + s.getData().clear(); + } - Platform.runLater(() -> { - for (XYChart.Series s : dataSeries) { - s.getData().clear(); - } - - dataSeries.clear(); - eventTypeToSeriesMap.clear(); - createSeries(); - }); + dataSeries.clear(); + eventTypeToSeriesMap.clear(); + createSeries(); } private static enum ScaleType implements Function { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java index 3d5980c12f..d0e4f57656 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java @@ -65,7 +65,6 @@ final class EventCountsChart extends StackedBarChart implements private static final Effect SELECTED_NODE_EFFECT = new Lighting(); private ContextMenu chartContextMenu; - private final TimeLineController controller; private final FilteredEventsModel filteredEvents; @@ -151,8 +150,6 @@ final class EventCountsChart extends StackedBarChart implements return new CountsIntervalSelector(this); } - - /** * used by {@link CountsViewPane#BarClickHandler} to close the context menu * when the bar menu is requested @@ -163,6 +160,10 @@ final class EventCountsChart extends StackedBarChart implements return chartContextMenu; } + public ObservableList getSelectedNodes() { + return selectedNodes; + } + void setRangeInfo(RangeDivisionInfo rangeInfo) { this.rangeInfo = rangeInfo; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index d6b0796c3e..cd2ee6b3ad 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -19,28 +19,21 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.MissingResourceException; +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.collections.SetChangeListener; import javafx.concurrent.Task; import javafx.fxml.FXML; -import javafx.geometry.Side; 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; import javafx.scene.control.CheckBox; -import javafx.scene.control.ContextMenu; import javafx.scene.control.CustomMenuItem; import javafx.scene.control.Label; import javafx.scene.control.MenuButton; @@ -52,31 +45,24 @@ import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.TreeItem; import javafx.scene.effect.Effect; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.stage.Modality; -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.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.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.ContextMenuProvider; -import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; -import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; -import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChartLane.HideDescriptionAction; -import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChartLane.UnhideDescriptionAction; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChart.HideDescriptionAction; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChart.UnhideDescriptionAction; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** @@ -91,7 +77,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; * EventTypeMap, and dataSets is all linked directly to the ClusterChart which * must only be manipulated on the JavaFx thread. */ -public class DetailViewPane extends AbstractVisualizationPane, PrimaryDetailsChart> implements ContextMenuProvider { +public class DetailViewPane extends AbstractVisualizationPane, DetailsChart> { private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName()); @@ -100,179 +86,77 @@ public class DetailViewPane extends AbstractVisualizationPane verticalAxis = new EventAxis<>("All Events"); private MultipleSelectionModel> treeSelectionModel; - private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - private final ScrollingLaneWrapper mainView; - private final ScrollingLaneWrapper pinnedView; private final DetailViewLayoutSettings layoutSettings; - private final PinnedEventsChart pinnedChart; - private final MasterDetailPane masterDetailPane; - private double dividerPosition = .1; - private static final int MIN_PINNED_LANE_HEIGHT = 50; - private ContextMenu contextMenu; - private IntervalSelector intervalSelector; - private final Pane rootPane; - - public ObservableList getEventStripes() { - return chart.getEventStripes(); - } - - private static class DetailIntervalSelector extends IntervalSelector { - - DetailIntervalSelector(ContextMenuProvider 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; - } - } - - @Override - public ContextMenu getContextMenu() { - return contextMenu; - } - - @Override - public ContextMenu getChartContextMenu(MouseEvent mouseEvent) throws MissingResourceException { - if (contextMenu != null) { - contextMenu.hide(); - } - - contextMenu = ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(mouseEvent), - TimeLineChart.newZoomHistoyActionGroup(controller))); - contextMenu.setAutoHide(true); - return contextMenu; - } - - DetailViewLayoutSettings getLayoutSettings() { - return layoutSettings; - } - - @Override - protected void resetData() { - for (XYChart.Series s : dataSeries) { - s.getData().forEach(chart::removeDataItem); - s.getData().clear(); - } - - mainView.reset(); - pinnedView.reset(); - - } public DetailViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region bottomLeftSpacer) { super(controller, partPane, contextPane, bottomLeftSpacer); layoutSettings = new DetailViewLayoutSettings(); + settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable()); //initialize chart; - chart = new PrimaryDetailsChart(this, detailsChartDateAxis, verticalAxis); - chart.setData(dataSeries); - setChartClickHandler(); //can we push this into chart + chart = new DetailsChart(this, detailsChartDateAxis, pinnedDateAxis, verticalAxis); +// setChartClickHandler(); //can we push this into chart + setCenter(chart); - mainView = new ScrollingLaneWrapper(chart); - pinnedChart = new PinnedEventsChart(this, pinnedDateAxis, new EventAxis<>("Pinned Events")); - pinnedView = new ScrollingLaneWrapper(pinnedChart); - pinnedChart.setMinHeight(MIN_PINNED_LANE_HEIGHT); - pinnedView.setMinHeight(MIN_PINNED_LANE_HEIGHT); - - masterDetailPane = new MasterDetailPane(Side.TOP, mainView, pinnedView, false); - masterDetailPane.setDividerPosition(dividerPosition); - masterDetailPane.prefHeightProperty().bind(heightProperty()); - masterDetailPane.prefWidthProperty().bind(widthProperty()); - rootPane = new Pane(masterDetailPane); - setCenter(rootPane); - - settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable()); - //bind layout fo axes and spacers +// //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())); - //maintain highlighted effect on correct nodes - highlightedNodes.addListener((ListChangeListener.Change> change) -> { - while (change.next()) { - change.getAddedSubList().forEach(eventNode -> eventNode.applyHighlightEffect()); - change.getRemoved().forEach(eventNode -> eventNode.clearHighlightEffect()); - } - }); - selectedNodes.addListener((Observable observable) -> { - highlightedNodes.clear(); - for (EventNodeBase selectedNode : selectedNodes) { - highlightedNodes.add(selectedNode); - } - controller.selectEventIDs(selectedNodes.stream() + chart.setHighlightPredicate(selectedNodes::contains); + getController().selectEventIDs(selectedNodes.stream() .flatMap(detailNode -> detailNode.getEventIDs().stream()) .collect(Collectors.toList())); }); - - filteredEvents.zoomParametersProperty().addListener(o -> { - clearIntervalSelector(); - selectedNodes.clear(); - controller.selectEventIDs(Collections.emptyList()); - }); - - TimeLineChart.MouseClickedHandler mouseClickedHandler = new TimeLineChart.MouseClickedHandler<>(this); - TimeLineChart.ChartDragHandler chartDragHandler = new TimeLineChart.ChartDragHandler<>(this); - configureMouseListeners(chart, mouseClickedHandler, chartDragHandler); - configureMouseListeners(pinnedChart, mouseClickedHandler, chartDragHandler); - } - @Override - public void clearIntervalSelector() { - rootPane.getChildren().remove(intervalSelector); - intervalSelector = null; - } - - @Override - public IntervalSelector newIntervalSelector() { - return new DetailIntervalSelector(this); - } - - @Override - public IntervalSelector getIntervalSelector() { - return intervalSelector; - } - - @Override - public void setIntervalSelector(IntervalSelector newIntervalSelector) { - intervalSelector = newIntervalSelector; - rootPane.getChildren().add(getIntervalSelector()); + public ObservableList getEventStripes() { + return chart.getEventStripes(); } public void setSelectionModel(MultipleSelectionModel> selectionModel) { this.treeSelectionModel = selectionModel; treeSelectionModel.getSelectedItems().addListener((Observable observable) -> { - highlightedNodes.clear(); - for (TreeItem tn : treeSelectionModel.getSelectedItems()) { - String description = tn.getValue().getDescription(); - for (EventNodeBase n : chart.getNodes(eventNode -> eventNode.hasDescription(description))) { - highlightedNodes.add(n); - } - for (EventNodeBase n : pinnedChart.getNodes(eventNode -> eventNode.hasDescription(description))) { - highlightedNodes.add(n); - } - } + Predicate> highlightPredicate = + treeSelectionModel.getSelectedItems().stream() + .map(TreeItem::getValue) + .map(TimeLineEvent::getDescription) + .map(new Function>>() { + @Override + public Predicate> apply(String description) { + return (EventNodeBase< ?> eventNode) -> eventNode.hasDescription(description); + } + }) + .reduce(selectedNodes::contains, Predicate::or); + chart.setHighlightPredicate(highlightPredicate); }); } + @Override + public Axis 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; @@ -283,11 +167,6 @@ public class DetailViewPane extends AbstractVisualizationPane getXAxis() { - return detailsChartDateAxis; - } - @Override protected double getTickSpacing() { return detailsChartDateAxis.getTickSpacing().get(); @@ -314,53 +193,13 @@ public class DetailViewPane extends AbstractVisualizationPane chartLane, final TimeLineChart.MouseClickedHandler mouseClickedHandler, final TimeLineChart.ChartDragHandler chartDragHandler) { - chartLane.setOnMousePressed(chartDragHandler); - chartLane.setOnMouseReleased(chartDragHandler); - chartLane.setOnMouseDragged(chartDragHandler); - chartLane.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedHandler); - } - private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); //NON-NLS - - private class PlaceMarkerAction extends Action { - - private GuideLine guideLine; - - @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(DetailViewPane.this); - guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); - addGuideLine(guideLine); - - } else { - guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); - } - }); - } - - } - - private void addGuideLine(GuideLine guideLine) { - rootPane.getChildren().add(guideLine); } private class DetailViewSettingsPane extends HBox { @@ -477,42 +316,11 @@ public class DetailViewPane extends AbstractVisualizationPane { - boolean selected = pinnedEventsToggle.isSelected(); - if (selected == false) { - dividerPosition = masterDetailPane.getDividerPosition(); - } - pinnedView.maxHeightProperty().unbind(); - masterDetailPane.setShowDetailNode(selected); - if (selected) { - pinnedView.setMinHeight(MIN_PINNED_LANE_HEIGHT); - pinnedView.maxHeightProperty().bind(pinnedChart.maxVScrollProperty().add(30)); - masterDetailPane.setDividerPosition(dividerPosition); - } - }); + pinnedEventsToggle.selectedProperty().bindBidirectional(chart.pinnedLaneShowing()); - controller.getPinnedEvents().addListener((SetChangeListener.Change change) -> { - boolean empty = change.getSet().isEmpty(); - pinnedEventsToggle.setSelected(empty == false); - }); - -// Platform.runLater(() -> { -// if (controller.getPinnedEvents().isEmpty() == false) { -// pinnedEventsToggle.setSelected(true); -// } -// }); } } - 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); - - } - @NbBundle.Messages({ "DetailViewPane.loggedTask.queryDb=Retreiving event data", "DetailViewPane.loggedTask.name=Updating Details View", @@ -567,12 +375,9 @@ public class DetailViewPane extends AbstractVisualizationPane 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; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java new file mode 100644 index 0000000000..d9a0d711ee --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java @@ -0,0 +1,390 @@ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import java.util.Arrays; +import java.util.Collections; +import java.util.MissingResourceException; +import java.util.function.Predicate; +import javafx.beans.Observable; +import javafx.beans.property.SimpleBooleanProperty; +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 class DetailsChart extends Control implements TimeLineChart { + + private final DetailViewPane parentPane; + private final DateAxis detailsChartDateAxis; + private final DateAxis pinnedDateAxis; + + private final Axis verticalAxis; + + private final SimpleBooleanProperty pinnedLaneShowing = new SimpleBooleanProperty(false); + private final SimpleObjectProperty< IntervalSelector> intervalSelector = new SimpleObjectProperty<>(); + private final SimpleObjectProperty>> highlightPredicate = new SimpleObjectProperty<>((x) -> false); + + DetailsChart(DetailViewPane parentPane, DateAxis detailsChartDateAxis, DateAxis pinnedDateAxis, Axis verticalAxis) { + this.parentPane = parentPane; + this.detailsChartDateAxis = detailsChartDateAxis; + this.verticalAxis = verticalAxis; + this.pinnedDateAxis = pinnedDateAxis; + getController().getPinnedEvents().addListener((SetChangeListener.Change change) -> { + pinnedLaneShowing.set(change.getSet().isEmpty() == false); + }); + + if (getController().getPinnedEvents().isEmpty() == false) { + pinnedLaneShowing.set(true); + } + + getController().getEventsModel().zoomParametersProperty().addListener(o -> { + clearIntervalSelector(); + getSelectedNodes().clear(); + getController().selectEventIDs(Collections.emptyList()); + }); + } + + public SimpleBooleanProperty pinnedLaneShowing() { + return pinnedLaneShowing; + } + + public boolean isPinnedLaneShowing() { + return pinnedLaneShowing.get(); + } + + public void setPinnedLaneShowing(boolean showing) { + pinnedLaneShowing.set(showing); + } + + DateTime getDateTimeForPosition(double layoutX) { + return ((DetailsChartSkin) getSkin()).getDateTimeForPosition(layoutX); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private final ObservableList eventStripes = FXCollections.observableArrayList(); + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + void addStripe(EventStripe stripe) { + eventStripes.add(stripe); + } + + void requestTimelineChartLayout() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + void clearGuideLine(GuideLine guideLine) { + guideLines.remove(guideLine); + } + + public ObservableList> getSelectedNodes() { + return parentPane.getSelectedNodes(); + } + + DetailViewLayoutSettings getLayoutSettings() { + return parentPane.getLayoutSettings(); + } + + void setHighlightPredicate(Predicate> highlightPredicate) { + this.highlightPredicate.set(highlightPredicate); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + void reset() { + eventStripes.clear(); + } + + private static class DetailIntervalSelector extends IntervalSelector { + + DetailIntervalSelector(IntervalSelectorProvider 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 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({"EventDetailChart.chartContextMenu.placeMarker.name=Place Marker"}) + PlaceMarkerAction(DetailsChart chart, MouseEvent clickEvent) { + super(Bundle.EventDetailChart_chartContextMenu_placeMarker_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 mouseClickedHandler, final TimeLineChart.ChartDragHandler chartDragHandler) { + chartLane.setOnMousePressed(chartDragHandler); + chartLane.setOnMouseReleased(chartDragHandler); + chartLane.setOnMouseDragged(chartDragHandler); + chartLane.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedHandler); + } + + public void clearIntervalSelector() { + intervalSelector.set(null); + } + + @Override + public IntervalSelector newIntervalSelector() { + return new DetailIntervalSelector(this); + } + + @Override + public IntervalSelector getIntervalSelector() { + return intervalSelector.get(); + } + + public SimpleObjectProperty> intervalSelector() { + return intervalSelector; + } + + @Override + public void setIntervalSelector(IntervalSelector newIntervalSelector) { + intervalSelector.set(newIntervalSelector); + } + + @Override + final public Axis getXAxis() { + return detailsChartDateAxis; + } + + @Override + final public TimeLineController getController() { + return parentPane.getController(); + } + + public ContextMenu getChartContextMenu(MouseEvent mouseEvent) throws MissingResourceException { + ContextMenu contextMenu = getContextMenu(); + if (contextMenu != null) { + contextMenu.hide(); + } + + setContextMenu(ActionUtils.createContextMenu(Arrays.asList( + new PlaceMarkerAction(this, mouseEvent), + TimeLineChart.newZoomHistoyActionGroup(getController())) + )); + getContextMenu().setAutoHide(true); + return getContextMenu(); + } + + @Override + protected Skin createDefaultSkin() { + return new DetailsChartSkin(this); + } + + ObservableList getEventStripes() { + 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.requestTimelineChartLayout()); + 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 { + + 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 intervalSelector; + + 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<>("Pinned Events")); + 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 mouseClickedHandler = new TimeLineChart.MouseClickedHandler<>(getSkinnable()); + TimeLineChart.ChartDragHandler chartDragHandler = new TimeLineChart.ChartDragHandler<>(getSkinnable()); + configureMouseListeners(primaryLane, mouseClickedHandler, chartDragHandler); + configureMouseListeners(pinnedLane, mouseClickedHandler, chartDragHandler); + + getSkinnable().pinnedLaneShowing.addListener(observable -> { + boolean selected = getSkinnable().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 change) -> { + if (change.wasRemoved()) { + rootPane.getChildren().remove(change.getElementRemoved()); + } + if (change.wasAdded()) { + rootPane.getChildren().add(change.getElementAdded()); + } + }); + } + + private Iterable> 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); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java index 814752874c..cfbe060e52 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java @@ -9,6 +9,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.google.common.collect.TreeRangeMap; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Map; @@ -17,13 +18,13 @@ 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.event.ActionEvent; import javafx.geometry.Insets; import javafx.scene.Cursor; import javafx.scene.Group; @@ -32,13 +33,9 @@ import javafx.scene.chart.Axis; 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 static javafx.scene.layout.Region.USE_PREF_SIZE; -import org.controlsfx.control.action.Action; import org.joda.time.DateTime; -import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; @@ -49,9 +46,7 @@ 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.IntervalSelector; -import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; +import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider; /** * @@ -60,17 +55,16 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; * } and {@link #removeDataItem(javafx.scene.chart.XYChart.Data) } to add and * remove data. */ -abstract class DetailsChartLane extends XYChart implements TimeLineChart { +abstract class DetailsChartLane extends XYChart implements ContextMenuProvider { private static final String STYLE_SHEET = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS - static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS - static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS + static final int MINIMUM_EVENT_NODE_GAP = 4; static final int MINIMUM_ROW_HEIGHT = 24; - private final DetailViewPane parentPane; + private final DetailsChart parentChart; final TimeLineController controller; final FilteredEventsModel filteredEvents; final DetailViewLayoutSettings layoutSettings; @@ -90,6 +84,17 @@ abstract class DetailsChartLane extends XYChart createNode(DetailsChartLane chart, TimeLineEvent event) { if (event.getEventIDs().size() == 1) { return new SingleEventNode(this, controller.getEventsModel().getEventById(Iterables.getOnlyElement(event.getEventIDs())), null); @@ -101,6 +106,7 @@ abstract class DetailsChartLane extends XYChart extends XYChart extends XYChart dateAxis, Axis verticalAxis, boolean useQuickHideFilters) { + DetailsChartLane(DetailsChart parentChart, Axis dateAxis, Axis verticalAxis, boolean useQuickHideFilters) { super(dateAxis, verticalAxis); - this.parentPane = parentPane; - this.layoutSettings = parentPane.getLayoutSettings(); - this.controller = parentPane.getController(); - this.selectedNodes = parentPane.getSelectedNodes(); + this.parentChart = parentChart; + this.layoutSettings = parentChart.getLayoutSettings(); + this.controller = parentChart.getController(); + this.selectedNodes = parentChart.getSelectedNodes(); this.filteredEvents = controller.getEventsModel(); this.useQuickHideFilters = useQuickHideFilters; + final Series series = new Series<>(); + setData(FXCollections.observableArrayList()); + getData().add(series); + Tooltip.install(this, AbstractVisualizationPane.getDefaultTooltip()); dateAxis.setAutoRanging(false); @@ -258,8 +267,8 @@ abstract class DetailsChartLane extends XYChart extends XYChart getEventStripes(); - @Override - public ContextMenu getContextMenu() { - return parentPane.getContextMenu(); - } - @Override protected void dataItemAdded(Series series, int itemIndex, Data item) { } @@ -294,24 +298,41 @@ abstract class DetailsChartLane extends XYChart series) { } - @Override - public IntervalSelector getIntervalSelector() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + /** + * add a dataitem to this chart + * + * @see note in main section of class JavaDoc + * + * @param data + */ + void addDataItem(Y event) { + + EventNodeBase eventNode = createNode(this, event); + eventMap.put(event, eventNode); + Platform.runLater(() -> { + events.add(event); + nodes.add(eventNode); + nodeGroup.getChildren().add(eventNode); +// data.setNode(eventNode); + + }); } - @Override - public void setIntervalSelector(IntervalSelector newIntervalSelector) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public IntervalSelector newIntervalSelector() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public void clearIntervalSelector() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + /** + * remove a data item from this chart + * + * @see note in main section of class JavaDoc + * + * @param data + */ + void removeDataItem(Y event) { + EventNodeBase removedNode = eventMap.remove(event); + Platform.runLater(() -> { + events.removeAll(Collections.singleton(event)); + events.removeAll(new StripeFlattener().apply(removedNode).collect(Collectors.toList())); + nodeGroup.getChildren().removeAll(removedNode); +// data.setNode(null); + }); } /** @@ -327,10 +348,6 @@ abstract class DetailsChartLane extends XYChart extends XYChart> getAllNodes() { + return getNodes((x) -> true); + } + /** * @return all the nodes that pass the given predicate */ @@ -348,18 +372,19 @@ abstract class DetailsChartLane extends XYChart, Stream>> stripeFlattener = new Function, Stream>>() { - @Override - public Stream> apply(EventNodeBase node) { - return Stream.concat( - Stream.of(node), - node.getSubNodes().stream().flatMap(this::apply)); - } - }; + @Override + public Stream> 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()); } + @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass double descriptionWidth; @@ -425,45 +450,10 @@ abstract class DetailsChartLane extends XYChart 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.requestTimelineChartLayout()); - chart.getController().getQuickHideFilters().add(testFilter); - return testFilter; - }); - descriptionFilter.setSelected(true); - }); - } + DetailsChart getParentChart() { + return parentChart; } - @NbBundle.Messages({"UnhideDescriptionAction.displayName=Unhide"}) - static class UnhideDescriptionAction extends Action { - - UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD, DetailsChartLane 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)) - ); - } - } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index 0be313272c..f172c5c229 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -206,7 +206,7 @@ final public class EventClusterNode extends MultiEventNodeBase extends StackPan private Button pinButton; private final Border SELECTION_BORDER; - EventNodeBase(Type ievent, EventNodeBase parent, DetailsChartLane chart) { - this.chartLane = chart; + EventNodeBase(Type ievent, EventNodeBase parent, DetailsChartLane chartLane) { + this.chartLane = chartLane; this.tlEvent = ievent; this.parentNode = parent; - sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); - eventsModel = chart.getController().getEventsModel(); + sleuthkitCase = chartLane.getController().getAutopsyCase().getSleuthkitCase(); + eventsModel = chartLane.getController().getEventsModel(); eventTypeImageView.setImage(getEventType().getFXImage()); descrLabel.setGraphic(eventTypeImageView); - if (chart.getController().getEventsModel().getEventTypeZoom() == EventTypeZoomLevel.SUB_TYPE) { + if (chartLane.getController().getEventsModel().getEventTypeZoom() == EventTypeZoomLevel.SUB_TYPE) { evtColor = getEventType().getColor(); } else { evtColor = getEventType().getBaseType().getColor(); @@ -159,7 +159,7 @@ public abstract class EventNodeBase extends StackPan //set up mouse hover effect and tooltip setOnMouseEntered(mouseEntered -> { - Tooltip.uninstall(chart, AbstractVisualizationPane.getDefaultTooltip()); + Tooltip.uninstall(chartLane, AbstractVisualizationPane.getDefaultTooltip()); showHoverControls(true); toFront(); }); @@ -168,7 +168,7 @@ public abstract class EventNodeBase extends StackPan if (parentNode != null) { parentNode.showHoverControls(true); } else { - Tooltip.install(chart, AbstractVisualizationPane.getDefaultTooltip()); + Tooltip.install(chartLane, AbstractVisualizationPane.getDefaultTooltip()); } }); setOnMouseClicked(new ClickHandler()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 9485ab334b..09be22f6d4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -33,7 +33,7 @@ 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 org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChartLane.HideDescriptionAction; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChart.HideDescriptionAction; import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configureActionButton; /** @@ -42,16 +42,19 @@ import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configu final public class EventStripeNode extends MultiEventNodeBase { private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName()); + + private Action newHideAction() { + return new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chartLane.getParentChart()); + } private Button hideButton; EventStripeNode(DetailsChartLane chartLane, EventStripe eventStripe, EventClusterNode parentNode) { super(chartLane, eventStripe, parentNode); setMinHeight(24); //setup description label - descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); - descrLabel.setPrefWidth(USE_COMPUTED_SIZE); + setAlignment(subNodePane, Pos.BOTTOM_LEFT); if (eventStripe.getClusters().size() > 1) { @@ -87,7 +90,7 @@ final public class EventStripeNode extends MultiEventNodeBase getActions() { - return Arrays.asList(new HideDescriptionAction(getDescription(), tlEvent.getDescriptionLoD(), chartLane)); + return Arrays.asList(newHideAction()); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java index fe7db93425..65af97c263 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java @@ -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 DetailViewPane 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(DetailViewPane chart) { + GuideLine(DetailsChart chart) { super(0, 0, 0, 0); this.chart = chart; Axis xAxis = chart.getXAxis(); @@ -64,7 +64,7 @@ class GuideLine extends Line { setOnMouseClicked(clickedEvent -> { if (clickedEvent.getButton() == MouseButton.SECONDARY - && clickedEvent.isStillSincePress() == false) { + && clickedEvent.isStillSincePress()) { chart.clearGuideLine(this); clickedEvent.consume(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java index dc381ff142..54dc09960a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java @@ -66,8 +66,8 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent chart, BundleType eventBundle, ParentNodeType parentNode) { - super(eventBundle, parentNode, chart); + MultiEventNodeBase(DetailsChartLane chartLane, BundleType eventBundle, ParentNodeType parentNode) { + super(eventBundle, parentNode, chartLane); this.descLOD.set(eventBundle.getDescriptionLoD()); @@ -88,9 +88,9 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent chart.requestTimelineChartLayout()); + heightProperty().addListener(heightProp -> chartLane.requestChartLayout()); Platform.runLater(() -> - setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()) + setLayoutX(chartLane.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()) ); //initialize info hbox @@ -101,7 +101,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent { - Tooltip.uninstall(chart, AbstractVisualizationPane.getDefaultTooltip()); + Tooltip.uninstall(chartLane, AbstractVisualizationPane.getDefaultTooltip()); showHoverControls(true); toFront(); }); @@ -110,7 +110,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent Platform.runLater(chartLane::requestTimelineChartLayout)); + timeline.setOnFinished(finished -> Platform.runLater(chartLane::requestChartLayout)); timeline.play(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java deleted file mode 100644 index 086dcfc5de..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChart.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2016 Basis Technology Corp. - * Contact: carrier sleuthkit 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.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.collections.SetChangeListener; -import javafx.scene.chart.Axis; -import javafx.scene.chart.XYChart; -import org.joda.time.DateTime; -import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; -import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; - -/** - * - */ -public final class PinnedEventsChart extends DetailsChartLane { - - /** - * - * @param controller the value of controller - * @param dateAxis the value of dateAxis - * @param verticalAxis the value of verticalAxis - * @param selectedNodes1 the value of selectedNodes1 - */ - PinnedEventsChart(DetailViewPane parentPane, DateAxis dateAxis, final Axis verticalAxis) { - super(parentPane, dateAxis, verticalAxis, false); - - final Series series = new Series<>(); - setData(FXCollections.observableArrayList()); - getData().add(series); - -// //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()); -// }); - getController().getPinnedEvents().addListener((SetChangeListener.Change change) -> { - if (change.wasAdded()) { - TimeLineEvent elementAdded = change.getElementAdded(); - Data data1 = new Data<>(new DateTime(elementAdded.getStartMillis()), elementAdded); - series.getData().add(data1); - addDataItem(data1); - } - if (change.wasRemoved()) { - TimeLineEvent elementRemoved = change.getElementRemoved(); - Data data1 = new Data<>(new DateTime(elementRemoved.getStartMillis()), elementRemoved); - series.getData().removeIf(t -> elementRemoved.equals(t.getYValue())); - removeDataItem(data1); - } - - requestChartLayout(); - }); - - for (TimeLineEvent event : getController().getPinnedEvents()) { - addDataItem(new XYChart.Data<>(new DateTime(event.getStartMillis()), event)); - } - - } - - @Override - public ObservableList getEventStripes() { - return FXCollections.emptyObservableList(); - } - - /** - * add a dataitem to this chart - * - * @see note in main section of class JavaDoc - * - * @param data - */ - void addDataItem(Data data) { - final TimeLineEvent event = data.getYValue(); - - EventNodeBase eventNode = createNode(PinnedEventsChart.this, event); - eventMap.put(event, eventNode); - Platform.runLater(() -> { - events.add(event); - nodes.add(eventNode); - nodeGroup.getChildren().add(eventNode); - data.setNode(eventNode); - - }); - } - - /** - * remove a data item from this chart - * - * @see note in main section of class JavaDoc - * - * @param data - */ - void removeDataItem(Data data) { - EventNodeBase removedNode = eventMap.remove(data.getYValue()); - Platform.runLater(() -> { - events.removeAll(data.getYValue()); - nodes.removeAll(removedNode); - nodeGroup.getChildren().removeAll(removedNode); - data.setNode(null); - }); - } - - @Override - void doAdditionalLayout() { - } - -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChartLane.java new file mode 100644 index 0000000000..fbe1a56bc0 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChartLane.java @@ -0,0 +1,70 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.SetChangeListener; +import javafx.scene.chart.Axis; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; + +/** + * + */ +public final class PinnedEventsChartLane extends DetailsChartLane { + + /** + * + * @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 verticalAxis) { + super(parentChart, dateAxis, verticalAxis, false); + +// final Series series = new Series<>(); +// setData(FXCollections.observableArrayList()); +// getData().add(series); + + getController().getPinnedEvents().addListener((SetChangeListener.Change change) -> { + if (change.wasAdded()) { + addDataItem(change.getElementAdded()); + } + if (change.wasRemoved()) { + removeDataItem(change.getElementRemoved()); + } + requestChartLayout(); + }); + + getController().getPinnedEvents().stream().forEach(this::addDataItem); + requestChartLayout(); + } + + @Override + public ObservableList getEventStripes() { + return FXCollections.emptyObservableList(); + } + + @Override + void doAdditionalLayout() { + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java similarity index 69% rename from Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java rename to Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java index 06a871ffd1..3fb7eef476 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java @@ -18,11 +18,8 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import com.google.common.collect.Iterables; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; -import javafx.application.Platform; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.chart.Axis; @@ -34,6 +31,7 @@ import org.joda.time.DateTime; 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 @@ -49,7 +47,7 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; * * //TODO: refactor the projected lines to a separate class. -jm */ -public final class PrimaryDetailsChart extends DetailsChartLane { +public final class PrimaryDetailsChartLane extends DetailsChartLane implements ContextMenuProvider { private static final int PROJECTED_LINE_Y_OFFSET = 5; private static final int PROJECTED_LINE_STROKE_WIDTH = 5; @@ -57,22 +55,27 @@ public final class PrimaryDetailsChart extends DetailsChartLane { @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private final Map projectionMap = new ConcurrentHashMap<>(); - PrimaryDetailsChart(DetailViewPane parentPane, DateAxis dateAxis, final Axis verticalAxis) { - super(parentPane, dateAxis, verticalAxis, true); + PrimaryDetailsChartLane(DetailsChart parentChart, DateAxis dateAxis, final Axis verticalAxis) { + super(parentChart, dateAxis, verticalAxis, true); // filteredEvents.zoomParametersProperty().addListener(o -> { // selectedNodes.clear(); // projectionMap.clear(); // controller.selectEventIDs(Collections.emptyList()); -// }); -// //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()); // }); //add listener for events that should trigger layout getController().getQuickHideFilters().addListener(layoutInvalidationListener); + parentChart.getEventStripes().addListener((ListChangeListener.Change change) -> { + while (change.next()) { + change.getAddedSubList().stream().forEach(this::addDataItem); + change.getRemoved().stream().forEach(this::removeDataItem); + } + requestChartLayout(); + }); + parentChart.getEventStripes().stream().forEach(this::addDataItem); + requestChartLayout(); + selectedNodes.addListener((ListChangeListener.Change> change) -> { while (change.next()) { change.getRemoved().forEach(removedNode -> { @@ -103,46 +106,6 @@ public final class PrimaryDetailsChart extends DetailsChartLane { return events; } - /** - * add a dataitem to this chart - * - * @see note in main section of class JavaDoc - * - * @param data - */ - void addDataItem(Data data) { - final EventStripe eventStripe = data.getYValue(); - EventNodeBase newNode; - if (eventStripe.getEventIDs().size() == 1) { - newNode = new SingleEventNode(this, controller.getEventsModel().getEventById(Iterables.getOnlyElement(eventStripe.getEventIDs())), null); - } else { - newNode = new EventStripeNode(PrimaryDetailsChart.this, eventStripe, null); - } - Platform.runLater(() -> { - events.add(eventStripe); - nodes.add(newNode); - nodeGroup.getChildren().add(newNode); - data.setNode(newNode); - }); - } - - /** - * remove a data item from this chart - * - * @see note in main section of class JavaDoc - * - * @param data - */ - void removeDataItem(Data data) { - Platform.runLater(() -> { - EventNodeBase removedNode = (EventNodeBase) data.getNode(); - events.removeAll(new StripeFlattener().apply(removedNode).collect(Collectors.toList())); - nodes.removeAll(removedNode); - nodeGroup.getChildren().removeAll(removedNode); - data.setNode(null); - }); - } - private double getParentXForEpochMillis(Long epochMillis) { return getXAxis().localToParent(getXForEpochMillis(epochMillis), 0).getX(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java index cb7bafd1ce..787ee2bbbd 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java @@ -111,7 +111,7 @@ final class SingleEventNode extends EventNodeBase { @Override void requestChartLayout() { - chartLane.requestTimelineChartLayout(); + chartLane.requestChartLayout(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java index 7a79b4946d..c619ee9c91 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java @@ -43,6 +43,7 @@ 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; @@ -199,13 +200,12 @@ final public class EventsTree extends BorderPane { if (getTreeItem() instanceof EventDescriptionTreeItem) { 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 { diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties index ffe39d6b4b..387cfb83e3 100644 --- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties +++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties @@ -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 diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties index aaaa4f0909..eac00c4b67 100644 --- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties +++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties @@ -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 From 5ab1fc33950cdb99f479ec66c14d35e21934c332 Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 9 Mar 2016 12:28:09 -0500 Subject: [PATCH 16/35] cleanup and simplify contextmenu API; fix contextMenu bug by not allowing interval selectors to be closed via right ClickHandler decouple DetailsChart from DetailViewPane.java more cleanup remove type parameter to ContextMenuProvider nodes implement ContextMenuProvider cleaned up node actions pulled a bit more common code into EventNodeBase move more code related to description visibility to EventNodeBase more cleanup cleaup descLOD property --- .../ui/AbstractVisualizationPane.java | 5 - .../timeline/ui/ContextMenuProvider.java | 10 +- .../autopsy/timeline/ui/IntervalSelector.java | 24 +- .../autopsy/timeline/ui/TimeLineChart.java | 30 +-- .../ui/countsview/CountsViewPane.java | 2 +- .../ui/countsview/EventCountsChart.java | 31 +-- .../ui/detailview/DetailViewPane.java | 45 ++-- .../timeline/ui/detailview/DetailsChart.java | 91 +++---- .../ui/detailview/DetailsChartLane.java | 94 +++----- ...s.java => DetailsChartLayoutSettings.java} | 23 +- .../ui/detailview/EventClusterNode.java | 87 +++---- .../timeline/ui/detailview/EventNodeBase.java | 223 +++++++++++------- .../ui/detailview/EventStripeNode.java | 39 +-- .../timeline/ui/detailview/GuideLine.java | 2 +- .../ui/detailview/MultiEventNodeBase.java | 73 ++---- .../ui/detailview/PinnedEventsChartLane.java | 15 +- .../detailview/PrimaryDetailsChartLane.java | 16 +- .../ui/detailview/SingleEventNode.java | 50 +--- 18 files changed, 369 insertions(+), 491 deletions(-) rename Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/{DetailViewLayoutSettings.java => DetailsChartLayoutSettings.java} (82%) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index 1022a925b9..c73a51f5ac 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -384,11 +384,6 @@ public abstract class AbstractVisualizationPane { -// -// }); -// } /** * add a {@link Text} node to the leaf container for the decluttered axis * labels diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java index 70499f790e..1710d5c734 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/ContextMenuProvider.java @@ -9,13 +9,11 @@ import javafx.scene.control.ContextMenu; import javafx.scene.input.MouseEvent; import org.sleuthkit.autopsy.timeline.TimeLineController; -public interface ContextMenuProvider { +public interface ContextMenuProvider { + TimeLineController getController(); - public TimeLineController getController(); - - ContextMenu getContextMenu(); - - ContextMenu getChartContextMenu(MouseEvent m); + void clearContextMenu(); + ContextMenu getContextMenu(MouseEvent m); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java index 62119aad26..92d769775d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java @@ -32,7 +32,6 @@ import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import org.controlsfx.control.action.Action; @@ -155,7 +154,11 @@ public abstract class IntervalSelector 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; @@ -189,18 +192,15 @@ public abstract class IntervalSelector extends BorderPane { mouseDrag.consume(); }); - ActionUtils.configureButton(new ZoomToSelectedIntervalAction(), zoomButton); - ActionUtils.configureButton(new ClearSelectedIntervalAction(), closeButton); - - setOnMouseClicked(mosueClick -> { - if (mosueClick.getButton() == MouseButton.SECONDARY) { - chart.clearIntervalSelector(); - mosueClick.consume(); - } else if (mosueClick.getClickCount() >= 2) { + setOnMouseClicked(mouseClick -> { + if (mouseClick.getClickCount() >= 2) { zoomToSelectedInterval(); - mosueClick.consume(); + mouseClick.consume(); } }); + + ActionUtils.configureButton(new ZoomToSelectedIntervalAction(), zoomButton); + ActionUtils.configureButton(new ClearSelectedIntervalAction(), closeButton); } private Point2D getLocalMouseCoords(MouseEvent mouseEvent) { @@ -245,7 +245,7 @@ public abstract class IntervalSelector 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}."}) private void updateStartAndEnd() { String startString = formatSpan(getSpanStart()); String endString = formatSpan(getSpanEnd()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java index c612decef3..22516eba78 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014-15 Basis Technology Corp. + * Copyright 2014-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -40,7 +40,7 @@ import org.sleuthkit.autopsy.timeline.ui.IntervalSelector.IntervalSelectorProvid * * @param the type of values along the horizontal axis */ -public interface TimeLineChart extends ContextMenuProvider, IntervalSelectorProvider { +public interface TimeLineChart extends ContextMenuProvider, IntervalSelectorProvider { ObservableList getSelectedNodes(); @@ -124,31 +124,17 @@ public interface TimeLineChart extends ContextMenuProvider, IntervalSelect @Override public void handle(MouseEvent mouseEvent) { - if (chart.getContextMenu() != null) { - chart.getContextMenu().hide(); - } - if (mouseEvent.getButton() == MouseButton.PRIMARY && mouseEvent.isStillSincePress()) { - chart.getSelectedNodes().clear(); - } else if (MouseEvent.MOUSE_CLICKED == mouseEvent.getEventType() && mouseEvent.isPopupTrigger() && mouseEvent.isStillSincePress()) { - ContextMenu chartContextMenu = chart.getChartContextMenu(mouseEvent); - chart.setOnMouseMoved(this); + if (MouseEvent.MOUSE_CLICKED == mouseEvent.getEventType() && mouseEvent.isPopupTrigger() && mouseEvent.isStillSincePress()) { + ContextMenu chartContextMenu = chart.getContextMenu(mouseEvent); chartContextMenu.show(chart, mouseEvent.getScreenX(), mouseEvent.getScreenY()); - mouseEvent.consume(); + 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(), diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java index da2de36761..8d37e68e33 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java @@ -103,7 +103,7 @@ public class CountsViewPane extends AbstractVisualizationPane 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(); } @@ -150,16 +163,6 @@ final class EventCountsChart extends StackedBarChart 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 - */ - public ContextMenu getContextMenu() { - return chartContextMenu; - } - public ObservableList getSelectedNodes() { return selectedNodes; } @@ -369,7 +372,7 @@ final class EventCountsChart extends StackedBarChart 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(); @@ -381,7 +384,7 @@ final class EventCountsChart extends StackedBarChart 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()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index cd2ee6b3ad..2363ae0b54 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -18,7 +18,6 @@ */ 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; @@ -86,17 +85,14 @@ public class DetailViewPane extends AbstractVisualizationPane verticalAxis = new EventAxis<>("All Events"); private MultipleSelectionModel> treeSelectionModel; - private final DetailViewLayoutSettings layoutSettings; public DetailViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region bottomLeftSpacer) { super(controller, partPane, contextPane, bottomLeftSpacer); - layoutSettings = new DetailViewLayoutSettings(); - settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable()); //initialize chart; - chart = new DetailsChart(this, detailsChartDateAxis, pinnedDateAxis, verticalAxis); -// setChartClickHandler(); //can we push this into chart + chart = new DetailsChart(controller, detailsChartDateAxis, pinnedDateAxis, verticalAxis, getSelectedNodes()); setCenter(chart); + settingsNodes = new DetailViewSettingsPane(chart.getLayoutSettings()).getChildrenUnmodifiable(); // //bind layout fo axes and spacers detailsChartDateAxis.getTickMarks().addListener((Observable observable) -> layoutDateLabels()); @@ -193,16 +189,12 @@ public class DetailViewPane extends AbstractVisualizationPane { if (truncateWidthSlider.isValueChanging() == false) { layoutSettings.truncateWidthProperty().set(truncateWidthSlider.getValue()); @@ -294,30 +290,23 @@ public class DetailViewPane extends AbstractVisualizationPane sleuthkit 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.Collections; import java.util.MissingResourceException; import java.util.function.Predicate; import javafx.beans.Observable; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -16,6 +32,7 @@ import javafx.geometry.Side; import javafx.scene.chart.Axis; import javafx.scene.control.ContextMenu; import javafx.scene.control.Control; +import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; import javafx.scene.image.Image; @@ -38,29 +55,31 @@ import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; -public class DetailsChart extends Control implements TimeLineChart { +public final class DetailsChart extends Control implements TimeLineChart { - private final DetailViewPane parentPane; private final DateAxis detailsChartDateAxis; private final DateAxis pinnedDateAxis; private final Axis verticalAxis; - private final SimpleBooleanProperty pinnedLaneShowing = new SimpleBooleanProperty(false); private final SimpleObjectProperty< IntervalSelector> intervalSelector = new SimpleObjectProperty<>(); private final SimpleObjectProperty>> highlightPredicate = new SimpleObjectProperty<>((x) -> false); + private final ObservableList> selectedNodes; + private final DetailsChartLayoutSettings layoutSettings = new DetailsChartLayoutSettings(); + private final TimeLineController controller; - DetailsChart(DetailViewPane parentPane, DateAxis detailsChartDateAxis, DateAxis pinnedDateAxis, Axis verticalAxis) { - this.parentPane = parentPane; + DetailsChart(TimeLineController controller, DateAxis detailsChartDateAxis, DateAxis pinnedDateAxis, Axis verticalAxis, ObservableList> selectedNodes) { + this.controller = controller; this.detailsChartDateAxis = detailsChartDateAxis; this.verticalAxis = verticalAxis; this.pinnedDateAxis = pinnedDateAxis; + this.selectedNodes = selectedNodes; getController().getPinnedEvents().addListener((SetChangeListener.Change change) -> { - pinnedLaneShowing.set(change.getSet().isEmpty() == false); + layoutSettings.setPinnedLaneShowing(change.getSet().isEmpty() == false); }); if (getController().getPinnedEvents().isEmpty() == false) { - pinnedLaneShowing.set(true); + layoutSettings.setPinnedLaneShowing(true); } getController().getEventsModel().zoomParametersProperty().addListener(o -> { @@ -70,18 +89,6 @@ public class DetailsChart extends Control implements TimeLineChart { }); } - public SimpleBooleanProperty pinnedLaneShowing() { - return pinnedLaneShowing; - } - - public boolean isPinnedLaneShowing() { - return pinnedLaneShowing.get(); - } - - public void setPinnedLaneShowing(boolean showing) { - pinnedLaneShowing.set(showing); - } - DateTime getDateTimeForPosition(double layoutX) { return ((DetailsChartSkin) getSkin()).getDateTimeForPosition(layoutX); } @@ -94,20 +101,16 @@ public class DetailsChart extends Control implements TimeLineChart { eventStripes.add(stripe); } - void requestTimelineChartLayout() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - void clearGuideLine(GuideLine guideLine) { guideLines.remove(guideLine); } public ObservableList> getSelectedNodes() { - return parentPane.getSelectedNodes(); + return selectedNodes; } - DetailViewLayoutSettings getLayoutSettings() { - return parentPane.getLayoutSettings(); + DetailsChartLayoutSettings getLayoutSettings() { + return layoutSettings; } void setHighlightPredicate(Predicate> highlightPredicate) { @@ -152,9 +155,9 @@ public class DetailsChart extends Control implements TimeLineChart { 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({"EventDetailChart.chartContextMenu.placeMarker.name=Place Marker"}) + @NbBundle.Messages({"PlaceMArkerAction.name=Place Marker"}) PlaceMarkerAction(DetailsChart chart, MouseEvent clickEvent) { - super(Bundle.EventDetailChart_chartContextMenu_placeMarker_name()); + super(Bundle.PlaceMArkerAction_name()); setGraphic(new ImageView(MARKER)); // NON-NLS setEventHandler(actionEvent -> { @@ -180,6 +183,7 @@ public class DetailsChart extends Control implements TimeLineChart { chartLane.setOnMousePressed(chartDragHandler); chartLane.setOnMouseReleased(chartDragHandler); chartLane.setOnMouseDragged(chartDragHandler); + chartLane.setOnMouseClicked(chartDragHandler); chartLane.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedHandler); } @@ -207,25 +211,30 @@ public class DetailsChart extends Control implements TimeLineChart { } @Override - final public Axis getXAxis() { + public Axis getXAxis() { return detailsChartDateAxis; } @Override - final public TimeLineController getController() { - return parentPane.getController(); + public TimeLineController getController() { + return controller; } - public ContextMenu getChartContextMenu(MouseEvent mouseEvent) throws MissingResourceException { + @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), - TimeLineChart.newZoomHistoyActionGroup(getController())) - )); + setContextMenu(new ContextMenu( + ActionUtils.createMenuItem(new PlaceMarkerAction(this, mouseEvent)), + new SeparatorMenuItem(), + ActionUtils.createMenuItem(TimeLineChart.newZoomHistoyActionGroup(getController())))); getContextMenu().setAutoHide(true); return getContextMenu(); } @@ -258,7 +267,7 @@ public class DetailsChart extends Control implements TimeLineChart { DescriptionFilter descriptionFilter = chart.getController().getQuickHideFilters().stream() .filter(testFilter::equals) .findFirst().orElseGet(() -> { - testFilter.selectedProperty().addListener(observable -> chart.requestTimelineChartLayout()); + testFilter.selectedProperty().addListener(observable -> chart.requestLayout()); chart.getController().getQuickHideFilters().add(testFilter); return testFilter; }); @@ -331,8 +340,8 @@ public class DetailsChart extends Control implements TimeLineChart { configureMouseListeners(primaryLane, mouseClickedHandler, chartDragHandler); configureMouseListeners(pinnedLane, mouseClickedHandler, chartDragHandler); - getSkinnable().pinnedLaneShowing.addListener(observable -> { - boolean selected = getSkinnable().isPinnedLaneShowing(); + getSkinnable().getLayoutSettings().pinnedLaneShowing().addListener(observable -> { + boolean selected = getSkinnable().getLayoutSettings().isPinnedLaneShowing(); if (selected == false) { dividerPosition = masterDetailPane.getDividerPosition(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java index cfbe060e52..f09d96fc6f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java @@ -9,7 +9,6 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.google.common.collect.TreeRangeMap; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Map; @@ -40,7 +39,6 @@ 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.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; @@ -55,44 +53,42 @@ import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider; * } and {@link #removeDataItem(javafx.scene.chart.XYChart.Data) } to add and * remove data. */ -abstract class DetailsChartLane extends XYChart implements ContextMenuProvider { +abstract class DetailsChartLane extends XYChart 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; - final TimeLineController controller; - final FilteredEventsModel filteredEvents; - final DetailViewLayoutSettings layoutSettings; - final ObservableList> selectedNodes; + private final TimeLineController controller; + private final DetailsChartLayoutSettings layoutSettings; + private final ObservableList> selectedNodes; - final Map> eventMap = new HashMap<>(); + private final Map> eventMap = new HashMap<>(); @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - final ObservableList events = FXCollections.observableArrayList(); 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 activeQuickHidefilters; - public boolean quickHideFiltersEnabled() { + boolean quickHideFiltersEnabled() { return useQuickHideFilters; } - @Override - public ContextMenu getChartContextMenu(MouseEvent clickEvent) { - return parentChart.getChartContextMenu(clickEvent); + public void clearContextMenu() { + parentChart.clearContextMenu(); } @Override - public ContextMenu getContextMenu() { - return parentChart.getContextMenu(); + public ContextMenu getContextMenu(MouseEvent clickEvent) { + return parentChart.getContextMenu(clickEvent); } EventNodeBase createNode(DetailsChartLane chart, TimeLineEvent event) { @@ -108,7 +104,7 @@ abstract class DetailsChartLane extends XYChart extends XYChart dateAxis, Axis verticalAxis, boolean useQuickHideFilters) { super(dateAxis, verticalAxis); @@ -160,9 +156,10 @@ abstract class DetailsChartLane extends XYChart series = new Series<>(); setData(FXCollections.observableArrayList()); getData().add(series); @@ -235,11 +232,9 @@ abstract class DetailsChartLane extends XYChart bundleNode : nodes) { - //is the node hiden by a quick hide filter? - boolean quickHide = useQuickHideFilters && activeQuickHidefilters.contains(bundleNode.getDescription()); - if (quickHide) { - //hide it and skip layout + 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 { @@ -276,78 +271,62 @@ abstract class DetailsChartLane extends XYChart getEventStripes(); - + @Deprecated @Override protected void dataItemAdded(Series series, int itemIndex, Data item) { } + @Deprecated @Override protected void dataItemRemoved(Data item, Series series) { } + @Deprecated @Override protected void dataItemChanged(Data item) { } + @Deprecated @Override protected void seriesAdded(Series series, int seriesIndex) { } + @Deprecated @Override protected void seriesRemoved(Series series) { } /** - * add a dataitem to this chart + * add an event to this chart * * @see note in main section of class JavaDoc * - * @param data + * @param event */ - void addDataItem(Y event) { - + void addEvent(Y event) { EventNodeBase eventNode = createNode(this, event); eventMap.put(event, eventNode); Platform.runLater(() -> { - events.add(event); nodes.add(eventNode); nodeGroup.getChildren().add(eventNode); -// data.setNode(eventNode); - }); } /** - * remove a data item from this chart + * remove an event from this chart * * @see note in main section of class JavaDoc * - * @param data + * @param event */ - void removeDataItem(Y event) { + void removeEvent(Y event) { EventNodeBase removedNode = eventMap.remove(event); Platform.runLater(() -> { - events.removeAll(Collections.singleton(event)); - events.removeAll(new StripeFlattener().apply(removedNode).collect(Collectors.toList())); + nodes.remove(removedNode); nodeGroup.getChildren().removeAll(removedNode); -// data.setNode(null); }); } - /** - * 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 getXAxis().getValueForDisplay(getXAxis().parentToLocal(x, 0).getX()); - } - /** * the group that all event nodes are added to. This facilitates scrolling * by allowing a single translation of this group. @@ -385,9 +364,6 @@ abstract class DetailsChartLane extends XYChart extends XYChart eventNode) { //make sure it is shown @@ -454,6 +428,4 @@ abstract class DetailsChartLane extends XYChart descrVisibility = - new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); + private final SimpleObjectProperty 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; } @@ -88,4 +104,5 @@ public class DetailViewLayoutSettings { DescriptionVisibility getDescrVisibility() { return descrVisibility.get(); } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index f172c5c229..a67e2ddcd2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -21,14 +21,12 @@ 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; @@ -67,8 +65,7 @@ import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; final public class EventClusterNode extends MultiEventNodeBase { private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); - 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 static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1); private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); @@ -80,8 +77,8 @@ final public class EventClusterNode extends MultiEventNodeBase transform = subNodes.stream().flatMap(new StripeFlattener()).collect(Collectors.toList()); -// ((EventDetailsChart) getChart()).getEventStripes().removeAll(transform); +// getChartLane().getParentChart().getEventStripes().removeAll(transform); subNodes.clear(); if (bundles.isEmpty()) { getChildren().setAll(subNodePane, infoHBox); - descLOD.set(getEventBundle().getDescriptionLoD()); + setDescriptionLOD(getEventBundle().getDescriptionLoD()); } else { -// ((EventDetailsChart) getChart()).getEventStripes().addAll(bundles); +// getChartLane().getParentChart().getEventStripes().addAll(bundles); subNodes.addAll(Lists.transform(bundles, 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 @@ -209,6 +185,7 @@ final public class EventClusterNode extends MultiEventNodeBase getActions() { - return Arrays.asList(new ExpandClusterAction(), - new CollapseClusterAction()); + Iterable getActions() { + return Iterables.concat( + super.getActions(), + Arrays.asList(new ExpandClusterAction(this), new CollapseClusterAction(this)) + ); } @Override EventHandler getDoubleClickHandler() { - return mouseEvent -> new ExpandClusterAction().handle(null); + return mouseEvent -> new ExpandClusterAction(this).handle(null); } - class ExpandClusterAction extends Action { + static private class ExpandClusterAction extends Action { + + private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N @NbBundle.Messages({"ExpandClusterAction.text=Expand"}) - ExpandClusterAction() { + 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.loadSubBundles(DescriptionLoD.RelativeDetail.MORE); } }); - disabledProperty().bind(descLOD.isEqualTo(DescriptionLoD.FULL)); + disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(DescriptionLoD.FULL)); } } - class CollapseClusterAction extends Action { + static private class CollapseClusterAction extends Action { + + private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N @NbBundle.Messages({"CollapseClusterAction.text=Collapse"}) - CollapseClusterAction() { + 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.loadSubBundles(DescriptionLoD.RelativeDetail.LESS); } }); - disabledProperty().bind(Bindings.createBooleanBinding(() -> nonNull(getEventCluster()) && descLOD.get() == getEventCluster().getDescriptionLoD(), descLOD)); + + disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(node.getEventCluster().getDescriptionLoD())); } } - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java index 3f9c6bedc4..00437cea4e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -19,10 +19,13 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; +import com.google.common.collect.Lists; +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; @@ -32,7 +35,6 @@ import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.application.Platform; -import javafx.beans.property.SimpleObjectProperty; import javafx.concurrent.Task; import javafx.event.EventHandler; import javafx.geometry.Insets; @@ -59,6 +61,7 @@ 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; @@ -71,9 +74,9 @@ 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.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.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; @@ -81,82 +84,80 @@ import org.sleuthkit.datamodel.TskCoreException; /** * */ -public abstract class EventNodeBase extends StackPane { - - static final Map dropShadowMap = new ConcurrentHashMap<>(); - - private static final Logger LOGGER = Logger.getLogger(EventNodeBase.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 +public abstract class EventNodeBase 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 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); } - - final Type tlEvent; - - final EventNodeBase parentNode; - - final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); - final SimpleObjectProperty descVisibility = new SimpleObjectProperty<>(); - + + 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_PIN); + final ImageView hashIV = new ImageView(HASH_HIT); final ImageView tagIV = new ImageView(TAG); - + final HBox controlsHBox = new HBox(5); final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV, controlsHBox); - + final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); - + final ImageView eventTypeImageView = new ImageView(); final SleuthkitCase sleuthkitCase; final FilteredEventsModel eventsModel; private Timeline timeline; private Button pinButton; private final Border SELECTION_BORDER; - - EventNodeBase(Type ievent, EventNodeBase parent, DetailsChartLane chartLane) { + + EventNodeBase(Type tlEvent, EventNodeBase parent, DetailsChartLane chartLane) { this.chartLane = chartLane; - this.tlEvent = ievent; + this.tlEvent = tlEvent; this.parentNode = parent; - + sleuthkitCase = chartLane.getController().getAutopsyCase().getSleuthkitCase(); eventsModel = chartLane.getController().getEventsModel(); eventTypeImageView.setImage(getEventType().getFXImage()); - + descrLabel.setGraphic(eventTypeImageView); - + 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)); - descVisibility.addListener(observable -> setDescriptionVisibiltiyImpl(descVisibility.get())); -// descVisibility.set(DescriptionVisibility.SHOWN); //trigger listener for initial value setBackground(defaultBackground); + Tooltip.install(this, this.tooltip); + //set up mouse hover effect and tooltip setOnMouseEntered(mouseEntered -> { Tooltip.uninstall(chartLane, AbstractVisualizationPane.getDefaultTooltip()); @@ -174,11 +175,20 @@ public abstract class EventNodeBase extends StackPan setOnMouseClicked(new ClickHandler()); show(controlsHBox, false); } - + public Type getEvent() { return tlEvent; } - + + @Override + public TimeLineController getController() { + return chartLane.getController(); + } + + public Optional> getParentNode() { + return Optional.ofNullable(parentNode); + } + DetailsChartLane getChartLane() { return chartLane; } @@ -189,7 +199,7 @@ public abstract class EventNodeBase extends StackPan public void setMaxDescriptionWidth(double w) { descrLabel.setMaxWidth(w); } - + public abstract List> getSubNodes(); /** @@ -200,7 +210,7 @@ public abstract class EventNodeBase extends StackPan public void applySelectionEffect(boolean applied) { setBorder(applied ? SELECTION_BORDER : null); } - + protected void layoutChildren() { super.layoutChildren(); } @@ -217,7 +227,7 @@ public abstract class EventNodeBase extends StackPan configureActionButton(pinButton); } } - + final void showHoverControls(final boolean showControls) { Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(), eventType -> new DropShadow(-10, eventType.getColor())); @@ -225,9 +235,9 @@ public abstract class EventNodeBase extends StackPan installTooltip(); enableTooltip(showControls); installActionButtons(); - + TimeLineController controller = getChartLane().getController(); - + if (controller.getPinnedEvents().contains(tlEvent)) { pinButton.setOnAction(actionEvent -> { new UnPinEventAction(controller, tlEvent).handle(actionEvent); @@ -241,7 +251,7 @@ public abstract class EventNodeBase extends StackPan }); pinButton.setGraphic(new ImageView(PIN)); } - + show(controlsHBox, showControls); if (parentNode != null) { parentNode.showHoverControls(false); @@ -270,7 +280,7 @@ public abstract class EventNodeBase extends StackPan { updateTitle(Bundle.EventNodeBase_toolTip_loading2()); } - + @Override protected String call() throws Exception { HashMap hashSetCounts = new HashMap<>(); @@ -290,7 +300,7 @@ public abstract class EventNodeBase extends StackPan String hashSetCountsString = hashSetCounts.entrySet().stream() .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) .collect(Collectors.joining("\n")); - + Map tagCounts = new HashMap<>(); if (tlEvent.getEventIDsWithTags().isEmpty() == false) { tagCounts.putAll(eventsModel.getTagCountsByTagName(tlEvent.getEventIDsWithTags())); @@ -298,14 +308,14 @@ public abstract class EventNodeBase extends StackPan String tagCountsString = tagCounts.entrySet().stream() .map((Map.Entry 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(); @@ -321,7 +331,7 @@ public abstract class EventNodeBase extends StackPan chartLane.getController().monitorTask(tooltTipTask); } } - + void enableTooltip(boolean toolTipEnabled) { if (toolTipEnabled) { Tooltip.install(this, tooltip); @@ -329,27 +339,27 @@ public abstract class EventNodeBase extends StackPan 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(); @@ -362,15 +372,38 @@ public abstract class EventNodeBase extends StackPan timeline.setOnFinished(finished -> Platform.runLater(this::requestChartLayout)); timeline.play(); } - - abstract void requestChartLayout(); - - void setDescriptionVisibility(DescriptionVisibility get) { - descVisibility.set(get); + + void requestChartLayout() { + getChartLane().requestChartLayout(); } - - abstract void setDescriptionVisibiltiyImpl(DescriptionVisibility get); - + + @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(""); + } + boolean hasDescription(String other) { return this.getDescription().startsWith(other); } @@ -389,23 +422,53 @@ public abstract class EventNodeBase extends StackPan setBackground(defaultBackground); } } - + void applyHighlightEffect() { applyHighlightEffect(true); } - + void clearHighlightEffect() { applyHighlightEffect(false); } - + abstract Collection getEventIDs(); - + abstract EventHandler getDoubleClickHandler(); - - abstract Collection getActions(); - + + Iterable 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); + } + static class PinEventAction extends Action { - + @NbBundle.Messages({"PinEventAction.text=Pin"}) PinEventAction(TimeLineController controller, TimeLineEvent event) { super(Bundle.PinEventAction_text()); @@ -413,9 +476,9 @@ public abstract class EventNodeBase extends StackPan setGraphic(new ImageView(PIN)); } } - + static class UnPinEventAction extends Action { - + @NbBundle.Messages({"UnPinEventAction.text=Unpin"}) UnPinEventAction(TimeLineController controller, TimeLineEvent event) { super(Bundle.UnPinEventAction_text()); @@ -428,9 +491,7 @@ public abstract class EventNodeBase extends StackPan * event handler used for mouse events on {@link EventNodeBase}s */ class ClickHandler implements EventHandler { - - private ContextMenu contextMenu; - + @Override public void handle(MouseEvent t) { if (t.getButton() == MouseButton.PRIMARY) { @@ -444,22 +505,10 @@ public abstract class EventNodeBase extends StackPan chartLane.getSelectedNodes().setAll(EventNodeBase.this); } t.consume(); - } else if (t.getButton() == MouseButton.SECONDARY) { - ContextMenu chartContextMenu = chartLane.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(EventNodeBase.this, t.getScreenX(), t.getScreenY()); + } else if (t.isPopupTrigger() && t.isStillSincePress()) { + getContextMenu(t).show(EventNodeBase.this, t.getScreenX(), t.getScreenY()); t.consume(); } } - } - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 09be22f6d4..8088c670bc 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -20,14 +20,12 @@ 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.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; @@ -44,7 +42,7 @@ final public class EventStripeNode extends MultiEventNodeBase getDoubleClickHandler() { return mouseEvent -> { @@ -139,7 +112,11 @@ final public class EventStripeNode extends MultiEventNodeBase getActions() { - return Arrays.asList(newHideAction()); + Iterable getActions() { + return Iterables.concat( + super.getActions(), + Arrays.asList(newHideAction()) + ); + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java index 65af97c263..bbdd039a11 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014-15 Basis Technology Corp. + * Copyright 2014-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java index 54dc09960a..017d231d64 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java @@ -18,31 +18,25 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import java.util.Collection; import java.util.List; import java.util.Set; -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.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.control.Tooltip; import javafx.scene.input.MouseEvent; import javafx.scene.layout.CornerRadii; import javafx.scene.layout.Pane; -import javafx.util.Duration; -import org.controlsfx.control.action.Action; 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.ui.AbstractVisualizationPane; import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; @@ -58,18 +52,14 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent> subNodes = FXCollections.observableArrayList(); final Pane subNodePane = new Pane(); - - private Timeline timeline; + private final ReadOnlyObjectWrapper descLOD = new ReadOnlyObjectWrapper<>(); MultiEventNodeBase(DetailsChartLane chartLane, BundleType eventBundle, ParentNodeType parentNode) { super(eventBundle, parentNode, chartLane); - this.descLOD.set(eventBundle.getDescriptionLoD()); - + setDescriptionLOD(eventBundle.getDescriptionLoD()); if (eventBundle.getEventIDsWithHashHits().isEmpty()) { show(hashIV, false); @@ -88,7 +78,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent chartLane.requestChartLayout()); + heightProperty().addListener(heightProp -> chartLane.requestLayout()); Platform.runLater(() -> setLayoutX(chartLane.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()) ); @@ -97,42 +87,27 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent { - Tooltip.uninstall(chartLane, AbstractVisualizationPane.getDefaultTooltip()); - showHoverControls(true); - toFront(); - }); - setOnMouseExited((MouseEvent event) -> { - showHoverControls(false); - if (parentNode != null) { - parentNode.showHoverControls(true); - } else { - Tooltip.install(chartLane, AbstractVisualizationPane.getDefaultTooltip()); - } - }); - setOnMouseClicked(new ClickHandler()); - Bindings.bindContent(subNodePane.getChildren(), subNodes); } - @Override - void requestChartLayout() { - getChartLane().requestChartLayout(); + public ReadOnlyObjectProperty descriptionLoDProperty() { + return descLOD.getReadOnlyProperty(); } - final DescriptionLoD getDescriptionLoD() { return descLOD.get(); } - public final BundleType getEventBundle() { - return tlEvent; + /** + * + */ + final void setDescriptionLOD(final DescriptionLoD descriptionLoD) { + descLOD.set(descriptionLoD); } - + public final BundleType getEventBundle() { + return getEvent(); + } @SuppressWarnings("unchecked") public List> getSubNodes() { @@ -143,8 +118,6 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent getEventIDs() { return getEventBundle().getEventIDs(); } @@ -162,21 +135,5 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent createChildNode(ParentType rawChild); - void animateTo(double xLeft, double yTop) { - if (timeline != null) { - timeline.stop(); - Platform.runLater(chartLane::requestChartLayout); - } - timeline = new Timeline(new KeyFrame(Duration.millis(100), - new KeyValue(layoutXProperty(), xLeft), - new KeyValue(layoutYProperty(), yTop)) - ); - timeline.setOnFinished(finished -> Platform.runLater(chartLane::requestChartLayout)); - timeline.play(); - } - abstract EventHandler getDoubleClickHandler(); - - abstract Collection getActions(); - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChartLane.java index fbe1a56bc0..28ae417ff2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PinnedEventsChartLane.java @@ -18,11 +18,8 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import javafx.collections.SetChangeListener; import javafx.scene.chart.Axis; -import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; /** @@ -43,26 +40,20 @@ public final class PinnedEventsChartLane extends DetailsChartLane // final Series series = new Series<>(); // setData(FXCollections.observableArrayList()); // getData().add(series); - getController().getPinnedEvents().addListener((SetChangeListener.Change change) -> { if (change.wasAdded()) { - addDataItem(change.getElementAdded()); + addEvent(change.getElementAdded()); } if (change.wasRemoved()) { - removeDataItem(change.getElementRemoved()); + removeEvent(change.getElementRemoved()); } requestChartLayout(); }); - getController().getPinnedEvents().stream().forEach(this::addDataItem); + getController().getPinnedEvents().stream().forEach(this::addEvent); requestChartLayout(); } - @Override - public ObservableList getEventStripes() { - return FXCollections.emptyObservableList(); - } - @Override void doAdditionalLayout() { } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java index 3fb7eef476..69b1dcfe6b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java @@ -21,13 +21,11 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; 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.joda.time.DateTime; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; @@ -47,7 +45,7 @@ import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider; * * //TODO: refactor the projected lines to a separate class. -jm */ -public final class PrimaryDetailsChartLane extends DetailsChartLane implements ContextMenuProvider { +public final class PrimaryDetailsChartLane extends DetailsChartLane implements ContextMenuProvider { private static final int PROJECTED_LINE_Y_OFFSET = 5; private static final int PROJECTED_LINE_STROKE_WIDTH = 5; @@ -68,15 +66,15 @@ public final class PrimaryDetailsChartLane extends DetailsChartLane parentChart.getEventStripes().addListener((ListChangeListener.Change change) -> { while (change.next()) { - change.getAddedSubList().stream().forEach(this::addDataItem); - change.getRemoved().stream().forEach(this::removeDataItem); + change.getAddedSubList().stream().forEach(this::addEvent); + change.getRemoved().stream().forEach(this::removeEvent); } requestChartLayout(); }); - parentChart.getEventStripes().stream().forEach(this::addDataItem); + parentChart.getEventStripes().stream().forEach(this::addEvent); requestChartLayout(); - selectedNodes.addListener((ListChangeListener.Change> change) -> { + getSelectedNodes().addListener((ListChangeListener.Change> change) -> { while (change.next()) { change.getRemoved().forEach(removedNode -> { removedNode.getEvent().getClusters().forEach(cluster -> { @@ -102,9 +100,7 @@ public final class PrimaryDetailsChartLane extends DetailsChartLane }); } - public ObservableList getEventStripes() { - return events; - } + private double getParentXForEpochMillis(Long epochMillis) { return getXAxis().localToParent(getXForEpochMillis(epochMillis), 0).getX(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java index 787ee2bbbd..7eb56dc128 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/SingleEventNode.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -33,16 +32,14 @@ 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.apache.commons.lang3.StringUtils; -import org.controlsfx.control.action.Action; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; /** * */ final class SingleEventNode extends EventNodeBase { + private static final Logger LOGGER = Logger.getLogger(SingleEventNode.class.getName()); static void show(Node b, boolean show) { @@ -58,16 +55,6 @@ final class SingleEventNode extends EventNodeBase { }; } - @Override - Collection getActions() { - TimeLineController controller = getChartLane().getController(); - if (controller.getPinnedEvents().contains(tlEvent)) { - return Arrays.asList(new UnPinEventAction(controller, tlEvent)); - } else { - return Arrays.asList(new PinEventAction(controller, tlEvent)); - } - } - SingleEventNode(DetailsChartLane chart, SingleEvent event, MultiEventNodeBase parent) { super(event, parent, chart); this.descrLabel.setText(event.getFullDescription()); @@ -92,7 +79,6 @@ final class SingleEventNode extends EventNodeBase { getChildren().add(infoHBox); } - @Override public List> getSubNodes() { return Collections.emptyList(); @@ -103,19 +89,11 @@ final class SingleEventNode extends EventNodeBase { super.layoutChildren(); //To change body of generated methods, choose Tools | Templates. } - @Override String getDescription() { - return tlEvent.getFullDescription(); + return getEvent().getFullDescription(); } - @Override - void requestChartLayout() { - chartLane.requestChartLayout(); - } - - - /** * @param w the maximum width the description label should have */ @@ -124,29 +102,7 @@ final class SingleEventNode extends EventNodeBase { descrLabel.setMaxWidth(w); } - @Override - void setDescriptionVisibiltiyImpl(DescriptionVisibility descrVis) { - - switch (descrVis) { - case HIDDEN: - countLabel.setText(null); - descrLabel.setText(""); - break; - case COUNT_ONLY: - countLabel.setText(null); - descrLabel.setText(""); - break; - default: - case SHOWN: - countLabel.setText(null); - String description = tlEvent.getFullDescription(); - description = parentNode != null - ? " ..." + StringUtils.substringAfter(description, parentNode.getDescription()) - : description; - descrLabel.setText(description); - break; - } - } + @Override Collection getEventIDs() { From f949a00093715b74e033b7920cde0430bdf40d8c Mon Sep 17 00:00:00 2001 From: jmillman Date: Thu, 10 Mar 2016 16:18:11 -0500 Subject: [PATCH 17/35] fix events tree; more cleanup --- .../timeline/datamodel/EventCluster.java | 17 +++--- .../timeline/datamodel/EventStripe.java | 38 ++++++++----- .../timeline/datamodel/MultiEvent.java | 2 +- .../autopsy/timeline/db/EventDB.java | 7 +-- .../ui/detailview/DetailViewPane.java | 2 +- .../timeline/ui/detailview/DetailsChart.java | 13 ++++- .../ui/detailview/EventClusterNode.java | 51 +++++++++-------- .../ui/detailview/EventStripeNode.java | 13 ++--- .../ui/detailview/MultiEventNodeBase.java | 20 +++---- .../detailview/PrimaryDetailsChartLane.java | 2 - .../tree/EventDescriptionTreeItem.java | 57 ++++++++----------- .../ui/detailview/tree/EventTypeTreeItem.java | 42 +++++++------- .../ui/detailview/tree/EventsTree.java | 15 ++--- .../{NavTreeItem.java => EventsTreeItem.java} | 8 ++- .../timeline/ui/detailview/tree/RootItem.java | 47 +++++++-------- 15 files changed, 162 insertions(+), 172 deletions(-) rename Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/{NavTreeItem.java => EventsTreeItem.java} (85%) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index c353979e3a..71a2d61482 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -22,7 +22,6 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Sets; import java.util.Comparator; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.SortedSet; @@ -33,9 +32,9 @@ import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** - * 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. + * 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 MultiEvent { @@ -120,7 +119,7 @@ public class EventCluster implements MultiEvent { } @Override - public Optional getParentBundle() { + public Optional getParent() { return Optional.ofNullable(parent); } @@ -181,9 +180,6 @@ public class EventCluster implements MultiEvent { * 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); } @@ -192,5 +188,8 @@ public class EventCluster implements MultiEvent { return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(this).build(); } - + @Override + public String toString() { + return "EventCluster{" + "description=" + description + ", eventIDs=" + eventIDs.size() + '}'; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index a9d8a6870a..bd160ac624 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -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; @@ -39,10 +39,10 @@ public final class EventStripe implements MultiEvent { 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 MultiEvent { private final ImmutableSet 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 clusters, ImmutableSet eventIDs, ImmutableSet tagged, ImmutableSet hashHits) { @@ -98,9 +100,10 @@ public final class EventStripe implements MultiEvent { 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 MultiEvent { 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 MultiEvent { .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 getParentBundle() { + public Optional getParent() { return Optional.ofNullable(parent); } + public Optional getParentStripe() { + if (getParent().isPresent()) { + return getParent().get().getParent(); + } else { + return Optional.empty(); + } + } + @Override public String getDescription() { return description; @@ -191,8 +202,7 @@ public final class EventStripe implements MultiEvent { @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 } - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java index c5e17b5542..eee68eada8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java @@ -45,7 +45,7 @@ public interface MultiEvent> extends TimeLineEv long getStartMillis(); - Optional getParentBundle(); + Optional getParent(); default int getSize() { return getEventIDs().size(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index 246fee2226..996c35ef3b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java @@ -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 hashHits = SQLHelper.unGroupConcat(rs.getString("hash_hits"), Long::valueOf); //NON-NLS Set 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()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 2363ae0b54..ca2e027b53 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -111,7 +111,7 @@ public class DetailViewPane extends AbstractVisualizationPane getEventStripes() { - return chart.getEventStripes(); + return chart.getAllNestedEventStripes(); } public void setSelectionModel(MultipleSelectionModel> selectionModel) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java index 86a5a0eb32..5af654be2b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java @@ -67,6 +67,7 @@ public final class DetailsChart extends Control implements TimeLineChart> selectedNodes; private final DetailsChartLayoutSettings layoutSettings = new DetailsChartLayoutSettings(); private final TimeLineController controller; + private ObservableList nestedEventStripes = FXCollections.observableArrayList(); DetailsChart(TimeLineController controller, DateAxis detailsChartDateAxis, DateAxis pinnedDateAxis, Axis verticalAxis, ObservableList> selectedNodes) { this.controller = controller; @@ -99,6 +100,7 @@ public final class DetailsChart extends Control implements TimeLineChart getAllNestedEventStripes() { + return nestedEventStripes; + } + + ObservableList getEventStripes() { + return eventStripes; } private static class DetailIntervalSelector extends IntervalSelector { @@ -244,7 +255,7 @@ public final class DetailsChart extends Control implements TimeLineChart getEventStripes() { + ObservableList getRootEventStripes() { return eventStripes; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index a67e2ddcd2..047c83a4c3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -25,6 +25,7 @@ import java.util.Collections; import java.util.List; import static java.util.Objects.nonNull; import java.util.concurrent.ExecutionException; +import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; import javafx.concurrent.Task; @@ -119,12 +120,11 @@ final public class EventClusterNode extends MultiEventNodeBase> loggedTask = new LoggedTask>(Bundle.EventStripeNode_loggedTask_name(), false) { + Task> loggedTask = new LoggedTask>(Bundle.EventClusterNode_loggedTask_name(), false) { private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); @Override protected List call() throws Exception { - List bundles; + List 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(new Function() { + + public EventStripe apply(EventStripe eventStripe) { + return eventStripe.withParent(getEvent()); + } + }) .collect(Collectors.toList()); } @Override protected void succeeded() { try { - List bundles = get(); + List newSubStripes = get(); //clear the existing subnodes - List transform = subNodes.stream().flatMap(new StripeFlattener()).collect(Collectors.toList()); -// getChartLane().getParentChart().getEventStripes().removeAll(transform); + List 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); - setDescriptionLOD(getEventBundle().getDescriptionLoD()); + setDescriptionLOD(getEvent().getDescriptionLoD()); } else { -// getChartLane().getParentChart().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)); setDescriptionLOD(loadedDescriptionLoD); } @@ -202,10 +207,6 @@ final public class EventClusterNode extends MultiEventNodeBase { if (node.getDescriptionLoD().moreDetailed() != null) { - node.loadSubBundles(DescriptionLoD.RelativeDetail.MORE); + node.loadSubStripes(DescriptionLoD.RelativeDetail.MORE); } }); disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(DescriptionLoD.FULL)); @@ -269,11 +270,11 @@ final public class EventClusterNode extends MultiEventNodeBase { if (node.getDescriptionLoD().lessDetailed() != null) { - node.loadSubBundles(DescriptionLoD.RelativeDetail.LESS); + node.loadSubStripes(DescriptionLoD.RelativeDetail.LESS); } }); - disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(node.getEventCluster().getDescriptionLoD())); + disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(node.getEvent().getDescriptionLoD())); } } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 8088c670bc..5da9be7912 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -57,20 +57,19 @@ final public class EventStripeNode extends MultiEventNodeBase 1) { for (EventCluster cluster : eventStripe.getClusters()) { - subNodes.add(createChildNode(cluster)); + subNodes.add(createChildNode(cluster.withParent(eventStripe))); } getChildren().addAll(new VBox(infoHBox, subNodePane)); } else { EventNodeBase childNode; - EventCluster cluster = Iterables.getOnlyElement(eventStripe.getClusters()); + EventCluster cluster = Iterables.getOnlyElement(eventStripe.getClusters()).withParent(eventStripe); if (cluster.getEventIDs().size() == 1) { - SingleEventNode singleEventNode = new SingleEventNode(getChartLane(), getChartLane().getController().getEventsModel().getEventById(Iterables.getOnlyElement(cluster.getEventIDs())), this); - childNode = singleEventNode; + childNode = createChildNode(cluster); } else { - EventClusterNode eventClusterNode = new EventClusterNode(getChartLane(), cluster, this); + EventClusterNode eventClusterNode = (EventClusterNode) createChildNode(cluster); eventClusterNode.installActionButtons(); - eventClusterNode.infoHBox.getChildren().remove(eventClusterNode.countLabel); controlsHBox.getChildren().addAll(eventClusterNode.minusButton, eventClusterNode.plusButton); + eventClusterNode.infoHBox.getChildren().remove(eventClusterNode.countLabel); childNode = eventClusterNode; } @@ -81,7 +80,7 @@ final public class EventStripeNode extends MultiEventNodeBase descLOD = new ReadOnlyObjectWrapper<>(); - MultiEventNodeBase(DetailsChartLane chartLane, BundleType eventBundle, ParentNodeType parentNode) { - super(eventBundle, parentNode, chartLane); - setDescriptionLOD(eventBundle.getDescriptionLoD()); + MultiEventNodeBase(DetailsChartLane chartLane, BundleType event, ParentNodeType parentNode) { + super(event, parentNode, chartLane); + setDescriptionLOD(event.getDescriptionLoD()); - if (eventBundle.getEventIDsWithHashHits().isEmpty()) { + if (event.getEventIDsWithHashHits().isEmpty()) { show(hashIV, false); } - if (eventBundle.getEventIDsWithTags().isEmpty()) { + if (event.getEventIDsWithTags().isEmpty()) { show(tagIV, false); } @@ -80,7 +80,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent chartLane.requestLayout()); Platform.runLater(() -> - setLayoutX(chartLane.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()) + setLayoutX(chartLane.getXAxis().getDisplayPosition(new DateTime(event.getStartMillis())) - getLayoutXCompensation()) ); //initialize info hbox @@ -105,21 +105,17 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent> getSubNodes() { return subNodes; } final String getDescription() { - return getEventBundle().getDescription(); + return getEvent().getDescription(); } final Set getEventIDs() { - return getEventBundle().getEventIDs(); + return getEvent().getEventIDs(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java index 69b1dcfe6b..403d30bec3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java @@ -100,8 +100,6 @@ public final class PrimaryDetailsChartLane extends DetailsChartLane }); } - - private double getParentXForEpochMillis(Long epochMillis) { return getXAxis().localToParent(getXForEpochMillis(epochMillis), 0).getX(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java index 85372311f7..faec34a6be 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -24,57 +24,49 @@ 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.MultiEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; /** * */ -class EventDescriptionTreeItem extends NavTreeItem { +class EventDescriptionTreeItem extends EventsTreeItem { /** * maps a description to the child item of this item with that description */ private final Map childMap = new HashMap<>(); - private final TimeLineEvent bundle; private Comparator> comparator = TreeComparator.Description; - public TimeLineEvent getEvent() { - return bundle; - } - - EventDescriptionTreeItem(TimeLineEvent g, Comparator> comp) { - bundle = g; + EventDescriptionTreeItem(EventStripe stripe, Comparator> comp) { comparator = comp; - setValue(g); - } - - @Override - public long getCount() { - return getValue().getSize(); + setValue(stripe); } @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - public void insert(Deque> path) { - MultiEvent 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 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> path) { - MultiEvent head = path.removeFirst(); - EventDescriptionTreeItem descTreeItem = childMap.get(head.getDescription()); + void remove(Deque 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); } @@ -94,14 +86,14 @@ class EventDescriptionTreeItem extends NavTreeItem { } @Override - public NavTreeItem findTreeItemForEvent(TimeLineEvent 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,5 +101,4 @@ class EventDescriptionTreeItem extends NavTreeItem { } return null; } - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java index 8f25e296b0..23ea3528e9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -25,10 +25,10 @@ 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.MultiEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; -class EventTypeTreeItem extends NavTreeItem { +class EventTypeTreeItem extends EventsTreeItem { /** * maps a description to the child item of this item with that description @@ -37,35 +37,31 @@ class EventTypeTreeItem extends NavTreeItem { private Comparator> comparator = TreeComparator.Description; - EventTypeTreeItem(MultiEvent g, Comparator> comp) { - setValue(g); + EventTypeTreeItem(EventStripe stripe, Comparator> comp) { + setValue(stripe); comparator = comp; } - @Override - public long getCount() { - return getValue().getSize(); - } - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - public void insert(Deque> path) { - MultiEvent 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 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> path) { - MultiEvent head = path.removeFirst(); + void remove(Deque path) { + EventStripe head = path.removeFirst(); EventDescriptionTreeItem descTreeItem = childMap.get(head.getDescription()); if (descTreeItem != null) { if (path.isEmpty() == false) { @@ -79,11 +75,11 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public NavTreeItem findTreeItemForEvent(TimeLineEvent t) { + public EventsTreeItem findTreeItemForEvent(TimeLineEvent t) { if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) { for (EventDescriptionTreeItem child : childMap.values()) { - final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); + final EventsTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); if (findTreeItemForEvent != null) { return findTreeItemForEvent; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java index c619ee9c91..5cd9647d02 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java @@ -49,6 +49,7 @@ 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.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; @@ -86,15 +87,11 @@ final public class EventsTree extends BorderPane { this.detailViewPane = detailViewPane; detailViewPane.setSelectionModel(eventsTree.getSelectionModel()); - detailViewPane.getEventStripes().addListener((ListChangeListener.Change> c) -> { + detailViewPane.getEventStripes().addListener((ListChangeListener.Change c) -> { //on jfx thread while (c.next()) { - for (MultiEvent bundle : c.getAddedSubList()) { - getRoot().insert(bundle); - } - for (MultiEvent bundle : c.getRemoved()) { - getRoot().remove(bundle); - } + c.getRemoved().forEach(getRoot()::remove); + c.getAddedSubList().forEach(getRoot()::insert); } }); @@ -116,9 +113,7 @@ 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 (MultiEvent bundle : detailViewPane.getEventStripes()) { - root.insert(bundle); - } + detailViewPane.getEventStripes().forEach(root::insert); eventsTree.setRoot(root); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTreeItem.java similarity index 85% rename from Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java rename to Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTreeItem.java index c3fd30ff9b..b118044f14 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTreeItem.java @@ -28,12 +28,14 @@ import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; * {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type, * description , count, etc. */ -abstract class NavTreeItem extends TreeItem { +abstract class EventsTreeItem extends TreeItem { - abstract long getCount(); + final long getSize() { + return getValue().getSize(); + } abstract void resort(Comparator> comp, Boolean recursive); - abstract NavTreeItem findTreeItemForEvent(TimeLineEvent t); + abstract EventsTreeItem findTreeItemForEvent(TimeLineEvent event); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java index 11cc15f11d..8b69f8b22f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -26,14 +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.MultiEvent; +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 @@ -49,52 +49,45 @@ class RootItem extends NavTreeItem { this.comparator = comp; } - @Override - public long getCount() { - return getValue().getSize(); - } - /** * 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(MultiEvent 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(MultiEvent 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< MultiEvent> getTreePath(MultiEvent g) { - Deque> path = new ArrayDeque<>(); - Optional> p = Optional.of(g); - - while (p.isPresent()) { - MultiEvent parent = p.get(); + static Deque< EventStripe> getTreePath(EventStripe event) { + Deque path = new ArrayDeque<>(); + path.addFirst(event); + Optional parentOptional = event.getParentStripe(); + while (parentOptional.isPresent()) { + EventStripe parent = parentOptional.get(); path.addFirst(parent); - p = parent.getParentBundle(); + parentOptional = parent.getParentStripe(); } - return path; } @@ -105,9 +98,9 @@ class RootItem extends NavTreeItem { } @Override - public NavTreeItem findTreeItemForEvent(TimeLineEvent 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; } From 3d41df5fc05b46c7bca9e01f79c4fd2f29ae816a Mon Sep 17 00:00:00 2001 From: jmillman Date: Fri, 11 Mar 2016 10:21:10 -0500 Subject: [PATCH 18/35] clear guidelines on zoom change --- .../autopsy/timeline/ui/detailview/DetailsChart.java | 7 ++++++- .../timeline/ui/detailview/PrimaryDetailsChartLane.java | 5 ----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java index 5af654be2b..0119955e5e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChart.java @@ -67,7 +67,7 @@ public final class DetailsChart extends Control implements TimeLineChart> selectedNodes; private final DetailsChartLayoutSettings layoutSettings = new DetailsChartLayoutSettings(); private final TimeLineController controller; - private ObservableList nestedEventStripes = FXCollections.observableArrayList(); + private final ObservableList nestedEventStripes = FXCollections.observableArrayList(); DetailsChart(TimeLineController controller, DateAxis detailsChartDateAxis, DateAxis pinnedDateAxis, Axis verticalAxis, ObservableList> selectedNodes) { this.controller = controller; @@ -85,6 +85,7 @@ public final class DetailsChart extends Control implements TimeLineChart { clearIntervalSelector(); + clearGuideLines(); getSelectedNodes().clear(); getController().selectEventIDs(Collections.emptyList()); }); @@ -103,6 +104,10 @@ public final class DetailsChart extends Control implements TimeLineChart PrimaryDetailsChartLane(DetailsChart parentChart, DateAxis dateAxis, final Axis verticalAxis) { super(parentChart, dateAxis, verticalAxis, true); -// filteredEvents.zoomParametersProperty().addListener(o -> { -// selectedNodes.clear(); -// projectionMap.clear(); -// controller.selectEventIDs(Collections.emptyList()); -// }); //add listener for events that should trigger layout getController().getQuickHideFilters().addListener(layoutInvalidationListener); From 64f8e8e282fef9534482e3e96af0ff9ab5358862 Mon Sep 17 00:00:00 2001 From: jmillman Date: Fri, 11 Mar 2016 11:35:17 -0500 Subject: [PATCH 19/35] polish the IntervalSelector UX, don't dismiss it if non-timerange zoom params change; --- .../ui/AbstractVisualizationPane.java | 1 - .../autopsy/timeline/ui/IntervalSelector.fxml | 17 ++++++++------ .../autopsy/timeline/ui/IntervalSelector.java | 9 +++++--- .../timeline/ui/detailview/DetailsChart.java | 22 +++++++++---------- .../timeline/ui/detailview/EventNodeBase.java | 2 -- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index c73a51f5ac..74573a011e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -544,7 +544,6 @@ public abstract class AbstractVisualizationPane { - chart.clearIntervalSelector(); resetData(); setDateAxisValues(axisValues); }); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml index a3885619d4..3d18c5e1c4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml @@ -1,12 +1,15 @@ - - - - - + + + + + + + + - + - - - -
- - - - - - - - - - - - - - - - - - -
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java index 2c5f1276fa..760f0a9bc3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java @@ -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 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 param) -> { - final TreeTableRow row = new TreeTableRow<>(); - - MenuItem all = new MenuItem(Bundle.Timeline_ui_filtering_menuItem_all()); - all.setOnAction(e -> { - row.getTreeItem().getParent().getChildren().forEach((TreeItem 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 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 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 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 param) -> { - final ListCell forList = new FilterCheckBoxCellFactory().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 getNewDiscriptionFilterListCell() { + final ListCell cell = new FilterCheckBoxCellFactory().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 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())); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeTableRow.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeTableRow.java new file mode 100644 index 0000000000..8b3f30be48 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeTableRow.java @@ -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 { + + @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, TreeTableRow> selectionPredicate; + + private final String displayName; + + private SelectionAction(String displayName, BiPredicate, TreeTableRow> predicate) { + this.selectionPredicate = predicate; + this.displayName = displayName; + } + + public void doSelection(TreeItem treeItem, TreeTableRow row) { + treeItem.getValue().setSelected(selectionPredicate.test(treeItem, row)); + } + + public String getDisplayName() { + return displayName; + } + } + + private static final class SelectActionsGroup extends ActionGroup { + + SelectActionsGroup(TreeTableRow 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 getRow() { + return row; + } + private final TreeTableRow row; + private final SelectionAction selectionAction; + + Select(SelectionAction strategy, TreeTableRow row) { + super(strategy.getDisplayName()); + this.row = row; + this.selectionAction = strategy; + setEventHandler(actionEvent -> row.getTreeItem().getParent().getChildren().forEach(this::doSelection)); + } + + private void doSelection(TreeItem treeItem) { + selectionAction.doSelection(treeItem, getRow()); + } + } +} From 37b6d605e032ee8a6b42f621184d4d4a7c225e33 Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 14 Mar 2016 15:46:47 -0400 Subject: [PATCH 21/35] fix selection bug for EventTypeTreeItems decouple EventsTree na DetailViewPane somewhat; improve EventsTreeItem interface; --- .../timeline/ui/VisualizationPanel.java | 70 +++++------ .../ui/detailview/DetailViewPane.java | 23 ++-- .../timeline/ui/detailview/DetailsChart.java | 10 +- .../ui/detailview/EventClusterNode.java | 8 +- .../timeline/ui/detailview/EventNodeBase.java | 3 - .../detailview/PrimaryDetailsChartLane.java | 4 +- .../tree/EventDescriptionTreeItem.java | 18 +++ .../ui/detailview/tree/EventTypeTreeItem.java | 18 ++- .../ui/detailview/tree/EventsTree.java | 104 ++++++++-------- .../ui/detailview/tree/EventsTreeItem.java | 9 +- .../timeline/ui/detailview/tree/RootItem.java | 11 ++ .../autopsy/timeline/utils/MappedList.java | 115 ++++++++++++++++++ 12 files changed, 270 insertions(+), 123 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/utils/MappedList.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java index bc7007d659..1fe72b5aa8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java @@ -384,9 +384,9 @@ final public class VisualizationPanel extends BorderPane { notificationPane.setContent(visualization); if (visualization instanceof DetailViewPane) { Platform.runLater(() -> { + ((DetailViewPane) visualization).setHighLightedEvents(eventsTree.getSelectedEvents()); eventsTree.setDetailViewPane((DetailViewPane) visualization); }); - } visualization.hasEvents.addListener((observable, oldValue, newValue) -> { if (newValue == false) { @@ -427,54 +427,54 @@ final public class VisualizationPanel extends BorderPane { histogramTask = new LoggedTask( NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.title"), true) { // NON-NLS - private final Lighting lighting = new Lighting(); + private final Lighting lighting = new Lighting(); @Override protected Void call() throws Exception { - updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.preparing")); // NON-NLS + updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.preparing")); // NON-NLS - long max = 0; - final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()); - final long lowerBound = rangeInfo.getLowerBound(); - final long upperBound = rangeInfo.getUpperBound(); - Interval timeRange = new Interval(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()), new DateTime(upperBound, TimeLineController.getJodaTimeZone())); + long max = 0; + final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()); + final long lowerBound = rangeInfo.getLowerBound(); + final long upperBound = rangeInfo.getUpperBound(); + Interval timeRange = new Interval(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()), new DateTime(upperBound, TimeLineController.getJodaTimeZone())); - //extend range to block bounderies (ie day, month, year) - int p = 0; // progress counter + //extend range to block bounderies (ie day, month, year) + int p = 0; // progress counter - //clear old data, and reset ranges and series - Platform.runLater(() -> { - updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.resetUI")); // NON-NLS + //clear old data, and reset ranges and series + Platform.runLater(() -> { + updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.resetUI")); // NON-NLS - }); + }); - ArrayList bins = new ArrayList<>(); + ArrayList bins = new ArrayList<>(); - DateTime start = timeRange.getStart(); - while (timeRange.contains(start)) { - if (isCancelled()) { - return null; - } - DateTime end = start.plus(rangeInfo.getPeriodSize().getPeriod()); - final Interval interval = new Interval(start, end); - //increment for next iteration + DateTime start = timeRange.getStart(); + while (timeRange.contains(start)) { + if (isCancelled()) { + return null; + } + DateTime end = start.plus(rangeInfo.getPeriodSize().getPeriod()); + final Interval interval = new Interval(start, end); + //increment for next iteration - start = end; + start = end; - updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.queryDb")); // NON-NLS - //query for current range - long count = filteredEvents.getEventCounts(interval).values().stream().mapToLong(Long::valueOf).sum(); - bins.add(count); + updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.queryDb")); // NON-NLS + //query for current range + long count = filteredEvents.getEventCounts(interval).values().stream().mapToLong(Long::valueOf).sum(); + bins.add(count); - max = Math.max(count, max); + max = Math.max(count, max); - final double fMax = Math.log(max); - final ArrayList fbins = new ArrayList<>(bins); - Platform.runLater(() -> { - updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.updateUI2")); // NON-NLS + final double fMax = Math.log(max); + final ArrayList fbins = new ArrayList<>(bins); + Platform.runLater(() -> { + updateMessage(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.histogramTask.updateUI2")); // NON-NLS - histogramBox.getChildren().clear(); + histogramBox.getChildren().clear(); for (Long bin : fbins) { if (isCancelled()) { @@ -499,7 +499,7 @@ final public class VisualizationPanel extends BorderPane { return null; } - }; + }; new Thread(histogramTask).start(); controller.monitorTask(histogramTask); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index ca2e027b53..c441a6b83c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -36,18 +36,17 @@ 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.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 javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; 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; @@ -62,6 +61,7 @@ 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; /** @@ -84,10 +84,11 @@ public class DetailViewPane extends AbstractVisualizationPane verticalAxis = new EventAxis<>("All Events"); - private MultipleSelectionModel> treeSelectionModel; + private final MappedList 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 DetailsChart(controller, detailsChartDateAxis, pinnedDateAxis, verticalAxis, getSelectedNodes()); @@ -110,22 +111,23 @@ public class DetailViewPane extends AbstractVisualizationPane getEventStripes() { + public ObservableList getAllEventStripes() { return chart.getAllNestedEventStripes(); } - public void setSelectionModel(MultipleSelectionModel> selectionModel) { - this.treeSelectionModel = selectionModel; + public ObservableList getSelectedEvents() { + return selectedEvents; + } - treeSelectionModel.getSelectedItems().addListener((Observable observable) -> { + public void setHighLightedEvents(ObservableList highlightedEvents) { + highlightedEvents.addListener((Observable observable) -> { Predicate> highlightPredicate = - treeSelectionModel.getSelectedItems().stream() - .map(TreeItem::getValue) + highlightedEvents.stream() .map(TimeLineEvent::getDescription) .map(new Function>>() { @Override public Predicate> apply(String description) { - return (EventNodeBase< ?> eventNode) -> eventNode.hasDescription(description); + return eventNode -> StringUtils.equalsIgnoreCase(eventNode.getDescription(), description); } }) .reduce(selectedNodes::contains, Predicate::or); @@ -382,4 +384,5 @@ public class DetailViewPane extends AbstractVisualizationPane nestedEventStripes = FXCollections.observableArrayList(); + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private final ObservableList eventStripes = FXCollections.observableArrayList(); + DetailsChart(TimeLineController controller, DateAxis detailsChartDateAxis, DateAxis pinnedDateAxis, Axis verticalAxis, ObservableList> selectedNodes) { this.controller = controller; this.detailsChartDateAxis = detailsChartDateAxis; @@ -97,9 +100,6 @@ public final class DetailsChart extends Control implements TimeLineChart eventStripes = FXCollections.observableArrayList(); - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) void addStripe(EventStripe stripe) { eventStripes.add(stripe); @@ -136,10 +136,6 @@ public final class DetailsChart extends Control implements TimeLineChart getEventStripes() { - return eventStripes; - } - private static class DetailIntervalSelector extends IntervalSelector { DetailIntervalSelector(IntervalSelectorProvider chart) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index 047c83a4c3..7ee68dcf72 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -25,7 +25,6 @@ import java.util.Collections; import java.util.List; import static java.util.Objects.nonNull; import java.util.concurrent.ExecutionException; -import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; import javafx.concurrent.Task; @@ -157,12 +156,7 @@ final public class EventClusterNode extends MultiEventNodeBase() { - - public EventStripe apply(EventStripe eventStripe) { - return eventStripe.withParent(getEvent()); - } - }) + .map(eventStripe -> eventStripe.withParent(getEvent())) .collect(Collectors.toList()); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java index 3f9cf95442..0bcd8c0c39 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -402,9 +402,6 @@ public abstract class EventNodeBase extends StackPan descrLabel.setText(""); } - boolean hasDescription(String other) { - return this.getDescription().startsWith(other); - } /** * apply the 'effect' to visually indicate highlighted nodes diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java index 8c598dcbcf..45aca19ff5 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java @@ -59,14 +59,14 @@ public final class PrimaryDetailsChartLane extends DetailsChartLane //add listener for events that should trigger layout getController().getQuickHideFilters().addListener(layoutInvalidationListener); - parentChart.getEventStripes().addListener((ListChangeListener.Change change) -> { + parentChart.getRootEventStripes().addListener((ListChangeListener.Change change) -> { while (change.next()) { change.getAddedSubList().stream().forEach(this::addEvent); change.getRemoved().stream().forEach(this::removeEvent); } requestChartLayout(); }); - parentChart.getEventStripes().stream().forEach(this::addEvent); + parentChart.getRootEventStripes().stream().forEach(this::addEvent); requestChartLayout(); getSelectedNodes().addListener((ListChangeListener.Change> change) -> { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java index faec34a6be..758fc4af91 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -28,6 +28,7 @@ import org.apache.commons.lang3.StringUtils; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; /** * @@ -101,4 +102,21 @@ class EventDescriptionTreeItem extends EventsTreeItem { } return null; } + + @Override + String getDisplayText() { + String text = getValue().getDescription() + " (" + getValue().getSize() + ")"; // NON-NLS + + TreeItem 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(); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java index 23ea3528e9..70ec2b97ff 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -27,6 +27,7 @@ import javafx.scene.control.TreeItem; import org.sleuthkit.autopsy.coreutils.ThreadConfined; 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 EventsTreeItem { @@ -36,9 +37,11 @@ class EventTypeTreeItem extends EventsTreeItem { private final Map childMap = new HashMap<>(); private Comparator> comparator = TreeComparator.Description; + private final EventType eventType; EventTypeTreeItem(EventStripe stripe, Comparator> comp) { - setValue(stripe); + setValue(null); + eventType = stripe.getEventType(); comparator = comp; } @@ -76,7 +79,7 @@ class EventTypeTreeItem extends EventsTreeItem { @Override public EventsTreeItem findTreeItemForEvent(TimeLineEvent t) { - if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) { + if (t.getEventType().getBaseType() == eventType.getBaseType()) { for (EventDescriptionTreeItem child : childMap.values()) { final EventsTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); @@ -96,4 +99,15 @@ class EventTypeTreeItem extends EventsTreeItem { childMap.values().forEach(ti -> ti.resort(comp, true)); } } + + @Override + String getDisplayText() { + return eventType.getDisplayName(); + } + + @Override + EventType getEventType() { + return eventType; + } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java index 5cd9647d02..849d13e530 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java @@ -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; @@ -50,9 +52,7 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; -import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent; 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.detailview.DetailViewPane; @@ -76,18 +76,17 @@ final public class EventsTree extends BorderPane { @FXML private ComboBox sortByBox; + private final ObservableList 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 c) -> { + detailViewPane.getAllEventStripes().addListener((ListChangeListener.Change c) -> { //on jfx thread while (c.next()) { c.getRemoved().forEach(getRoot()::remove); @@ -97,10 +96,10 @@ final public class EventsTree extends BorderPane { setRoot(); - detailViewPane.getSelectedNodes().addListener((Observable observable) -> { + detailViewPane.getSelectedEvents().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); - detailViewPane.getSelectedNodes().forEach(eventBundleNode -> { - eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventBundleNode.getEvent())); + detailViewPane.getSelectedEvents().forEach(event -> { + eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(event)); }); }); @@ -113,9 +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())); - detailViewPane.getEventStripes().forEach(root::insert); + detailViewPane.getAllEventStripes().forEach(root::insert); eventsTree.setRoot(root); - } @FXML @@ -127,19 +125,30 @@ 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> change) -> { + while (change.next()) { + change.getRemoved().stream().map(TreeItem::getValue).forEach(selectedEvents::remove); + change.getAddedSubList().stream().map(TreeItem::getValue).filter(Objects::nonNull).forEach(selectedEvents::add); + } + }); + eventsTreeLabel.setText(Bundle.EventsTree_Label_text()); } + public ObservableList getSelectedEvents() { + return selectedEvents; + } + /** - * A tree cell to display {@link MultiEvent}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 { @@ -148,7 +157,7 @@ final public class EventsTree extends BorderPane { 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); @@ -159,40 +168,32 @@ final public class EventsTree extends BorderPane { @Override 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 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.getSize() + ")"; // NON-NLS - TreeItem 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 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) { Action action = hidden.get() @@ -225,25 +226,22 @@ final public class EventsTree extends BorderPane { } } - private void updateHiddenState(TimeLineEvent item) { - TreeItem 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)); } } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTreeItem.java index b118044f14..c5170fbd8c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTreeItem.java @@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; import javafx.scene.control.TreeItem; 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 @@ -30,12 +31,12 @@ import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; */ abstract class EventsTreeItem extends TreeItem { - final long getSize() { - return getValue().getSize(); - } - abstract void resort(Comparator> comp, Boolean recursive); abstract EventsTreeItem findTreeItemForEvent(TimeLineEvent event); + abstract String getDisplayText(); + + abstract EventType getEventType(); + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java index 8b69f8b22f..aa7484be0e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -107,4 +107,15 @@ class RootItem extends EventsTreeItem { } return null; } + + @Override + String getDisplayText() { + return ""; + } + + @Override + EventType getEventType() { + return null; + } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/utils/MappedList.java b/Core/src/org/sleuthkit/autopsy/timeline/utils/MappedList.java new file mode 100644 index 0000000000..9e2ab30ddd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/utils/MappedList.java @@ -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 extends TransformationList { + private final Function mapper; + + public MappedList(ObservableList source, Function 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 c) { + fireChange(new ListChangeListener.Change(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 getRemoved() { + ArrayList 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(); + } + }); + } + +} From 33ec74850664439201a153a8a8a8315015af3d21 Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 16 Mar 2016 10:48:01 -0400 Subject: [PATCH 22/35] fix hover bug in interval selector --- .../src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml | 2 +- .../src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml index 3d18c5e1c4..754b00e35d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.fxml @@ -25,7 +25,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java index 38c5004ac4..fe8635a31c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java @@ -109,7 +109,7 @@ public abstract class IntervalSelector 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); From fdf33d541665876a4f3836777a5096ead8c9e96a Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 16 Mar 2016 11:45:56 -0400 Subject: [PATCH 23/35] clear interval selector in countsChart when the time range changes --- .../autopsy/timeline/ui/countsview/EventCountsChart.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java index c1cdafa771..26569ab47c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java @@ -108,6 +108,10 @@ final class EventCountsChart extends StackedBarChart implements setOnMouseClicked(new MouseClickedHandler<>(this)); this.selectedNodes = selectedNodes; + + getController().getEventsModel().timeRangeProperty().addListener(o -> { + clearIntervalSelector(); + }); } @Override From 86516951512560ba385388fdc20ac681f69a667e Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 16 Mar 2016 11:48:28 -0400 Subject: [PATCH 24/35] move tag and hash icons to the left of event nodes --- .../autopsy/timeline/ui/detailview/EventNodeBase.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java index 0bcd8c0c39..a6dd732b78 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -119,13 +119,12 @@ public abstract class EventNodeBase extends StackPan final Label descrLabel = new Label(); final ImageView hashIV = new ImageView(HASH_HIT); final ImageView tagIV = new ImageView(TAG); - - final HBox controlsHBox = new HBox(5); - final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV, controlsHBox); + final ImageView eventTypeImageView = new ImageView(); final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading()); - final ImageView eventTypeImageView = new ImageView(); + 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; @@ -141,8 +140,7 @@ public abstract class EventNodeBase extends StackPan eventsModel = chartLane.getController().getEventsModel(); eventTypeImageView.setImage(getEventType().getFXImage()); - descrLabel.setGraphic(eventTypeImageView); - +// descrLabel.setGraphic(); if (chartLane.getController().getEventsModel().getEventTypeZoom() == EventTypeZoomLevel.SUB_TYPE) { evtColor = getEventType().getColor(); } else { @@ -402,7 +400,6 @@ public abstract class EventNodeBase extends StackPan descrLabel.setText(""); } - /** * apply the 'effect' to visually indicate highlighted nodes * From e96607db26a9c0801667d168d3357c8d67697a0d Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 16 Mar 2016 13:05:53 -0400 Subject: [PATCH 25/35] cleanup; move one more bit of code up to EventNodeBase; prepare EventnodeBase for listening to tag events. --- .../datamodel/FilteredEventsModel.java | 23 ++++++---- .../timeline/events/TagsAddedEvent.java | 15 +++++++ .../timeline/events/TagsDeletedEvent.java | 17 ++++++++ .../timeline/events/TagsUpdatedEvent.java | 4 +- .../timeline/ui/detailview/EventNodeBase.java | 42 +++++++++++++++++-- .../ui/detailview/MultiEventNodeBase.java | 8 +--- .../ui/detailview/SingleEventNode.java | 8 ---- 7 files changed, 88 insertions(+), 29 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/events/TagsAddedEvent.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/events/TagsDeletedEvent.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index 05401511a9..d53316ef4d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -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; @@ -351,14 +352,14 @@ public final class FilteredEventsModel { ContentTag contentTag = evt.getAddedTag(); Content content = contentTag.getContent(); Set 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 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 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 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 updatedEventIDs) { + private boolean postTagsAdded(Set updatedEventIDs) { boolean tagsUpdated = !updatedEventIDs.isEmpty(); if (tagsUpdated) { - eventbus.post(new TagsUpdatedEvent(updatedEventIDs)); + eventbus.post(new TagsAddedEvent(updatedEventIDs)); + } + return tagsUpdated; + } + + private boolean postTagsDeleted(Set updatedEventIDs) { + boolean tagsUpdated = !updatedEventIDs.isEmpty(); + if (tagsUpdated) { + eventbus.post(new TagsDeletedEvent(updatedEventIDs)); } return tagsUpdated; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/TagsAddedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/TagsAddedEvent.java new file mode 100644 index 0000000000..2ff4474429 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/events/TagsAddedEvent.java @@ -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 updatedEventIDs) { + super(updatedEventIDs); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/TagsDeletedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/TagsDeletedEvent.java new file mode 100644 index 0000000000..8f7791910f --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/events/TagsDeletedEvent.java @@ -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 updatedEventIDs) { + super(updatedEventIDs); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/TagsUpdatedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/TagsUpdatedEvent.java index 167069837c..7d38458332 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/events/TagsUpdatedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/events/TagsUpdatedEvent.java @@ -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 updatedEventIDs; - public ImmutableSet getUpdatedEventIDs() { return ImmutableSet.copyOf(updatedEventIDs); } - public TagsUpdatedEvent(Set updatedEventIDs) { this.updatedEventIDs = updatedEventIDs; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java index a6dd732b78..66bdb7fdf3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventNodeBase.java @@ -20,6 +20,8 @@ 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; @@ -73,6 +75,8 @@ 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; @@ -140,7 +144,14 @@ public abstract class EventNodeBase extends StackPan eventsModel = chartLane.getController().getEventsModel(); eventTypeImageView.setImage(getEventType().getFXImage()); -// descrLabel.setGraphic(); + 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 { @@ -459,7 +470,30 @@ public abstract class EventNodeBase extends StackPan descrLabel.setText(description); } - static class PinEventAction extends Action { + @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 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) { @@ -469,7 +503,7 @@ public abstract class EventNodeBase extends StackPan } } - static class UnPinEventAction extends Action { + private static class UnPinEventAction extends Action { @NbBundle.Messages({"UnPinEventAction.text=Unpin"}) UnPinEventAction(TimeLineController controller, TimeLineEvent event) { @@ -482,7 +516,7 @@ public abstract class EventNodeBase extends StackPan /** * event handler used for mouse events on {@link EventNodeBase}s */ - class ClickHandler implements EventHandler { + private class ClickHandler implements EventHandler { @Override public void handle(MouseEvent t) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java index aee07ef17d..9ff46298bb 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/MultiEventNodeBase.java @@ -37,7 +37,6 @@ import org.joda.time.DateTime; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.datamodel.MultiEvent; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** @@ -61,12 +60,7 @@ public abstract class MultiEventNodeBase< BundleType extends MultiEvent { descrLabel.setPrefWidth(USE_COMPUTED_SIZE); setMinHeight(24); setAlignment(Pos.CENTER_LEFT); - if (event.getEventIDsWithHashHits().isEmpty()) { - show(hashIV, false); - } - if (event.getEventIDsWithTags().isEmpty()) { - show(tagIV, false); - } final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); setBorder(clusterBorder); @@ -102,8 +96,6 @@ final class SingleEventNode extends EventNodeBase { descrLabel.setMaxWidth(w); } - - @Override Collection getEventIDs() { return getEvent().getEventIDs(); From 9ac592e1f022d9d197d6b2771d66b65a14d589b9 Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 16 Mar 2016 13:25:39 -0400 Subject: [PATCH 26/35] internationalize DetailsChart lane labels --- .../autopsy/timeline/ui/detailview/Bundle.properties | 7 ++++--- .../autopsy/timeline/ui/detailview/DetailViewPane.java | 8 +++++--- .../autopsy/timeline/ui/detailview/DetailsChart.java | 3 ++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties index 6868facc84..eb769d2656 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties @@ -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 \ No newline at end of file +DetailViewPane.hiddenRadio.text=Hide Description + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index c441a6b83c..af46818611 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -82,7 +82,9 @@ public class DetailViewPane extends AbstractVisualizationPane verticalAxis = new EventAxis<>("All Events"); + + @NbBundle.Messages("DetailViewPane.primaryLaneLabel.text=All Events (Filtered)") + private final Axis verticalAxis = new EventAxis<>(Bundle.DetailViewPane_primaryLaneLabel_text()); private final MappedList selectedEvents; @@ -299,8 +301,8 @@ public class DetailViewPane extends AbstractVisualizationPane intervalSelector; + @NbBundle.Messages("DetailViewPane.pinnedLaneLabel.text=Pinned Events") DetailsChartSkin(DetailsChart chart) { super(chart); //initialize chart; @@ -327,7 +328,7 @@ public final class DetailsChart extends Control implements TimeLineChart("Pinned Events")); + 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()); From ef751ea7cb946a8535b5c45858e7d673d9d99777 Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 21 Mar 2016 12:58:05 -0400 Subject: [PATCH 27/35] disable subfilters if parent filter is unselected --- .../timeline/filters/AbstractFilter.java | 2 +- .../timeline/filters/CompoundFilter.java | 4 +-- .../timeline/filters/DataSourcesFilter.java | 20 +++++++-------- .../timeline/filters/HashHitsFilter.java | 23 +++++++---------- .../timeline/filters/HashSetFilter.java | 1 - .../autopsy/timeline/filters/TagsFilter.java | 24 ++++++++---------- .../autopsy/timeline/filters/TypeFilter.java | 25 +++++++++++++------ .../autopsy/timeline/filters/UnionFilter.java | 16 +++++++++++- .../filtering/FilterCheckBoxCellFactory.java | 3 ++- 9 files changed, 65 insertions(+), 53 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java index c51b3a82ed..6c9e1b00ed 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java @@ -60,7 +60,7 @@ public abstract class AbstractFilter implements Filter { @Override public boolean isDisabled() { - return disabled.get(); + return disabledProperty().get(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java index bd88a6d90b..055c1c8748 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java @@ -64,8 +64,8 @@ public abstract class CompoundFilter 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 -> { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourcesFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourcesFilter.java index 0da318e80d..baeb3b7e61 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourcesFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourcesFilter.java @@ -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 { 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 { } 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 { return Bindings.or(super.disabledProperty(), Bindings.size(getSubFilters()).lessThanOrEqualTo(1)); } + @Override + Predicate getDuplicatePredicate(DataSourceFilter subfilter) { + return dataSourcefilter -> dataSourcefilter.getDataSourceID() == subfilter.getDataSourceID(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/HashHitsFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/HashHitsFilter.java index 371f32dcdc..c2eb196b79 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/HashHitsFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/HashHitsFilter.java @@ -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 { 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 { 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 getDuplicatePredicate(HashSetFilter subfilter) { + return hashSetFilter -> subfilter.getHashSetID() == hashSetFilter.getHashSetID(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/HashSetFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/HashSetFilter.java index ad6df42d85..f2d5b82a36 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/HashSetFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/HashSetFilter.java @@ -84,5 +84,4 @@ public class HashSetFilter extends AbstractFilter { } return isSelected() == other.isSelected(); } - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/TagsFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/TagsFilter.java index 4c92a3b02a..973b82a19d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/TagsFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/TagsFilter.java @@ -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 { 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 { 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 { public ObservableBooleanValue disabledProperty() { return Bindings.or(super.disabledProperty(), Bindings.isEmpty(getSubFilters())); } + + @Override + Predicate getDuplicatePredicate(TagNameFilter subfilter) { + return tagNameFilter -> subfilter.getTagName().equals(tagNameFilter.getTagName()); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java index 4968e92e80..d3dc05985e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java @@ -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 { + static private final Comparator comparator = Comparator.comparing(TypeFilter::getEventType, EventType.getComparator()); + /** * the event type this filter passes */ @@ -52,7 +56,7 @@ public class TypeFilter extends UnionFilter { 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 { @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 { hash = 67 * hash + Objects.hashCode(this.eventType); return hash; } + + @Override + Predicate getDuplicatePredicate(TypeFilter subfilter) { + return t -> subfilter.getEventType().equals(t.eventType); + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java index 27bec42c72..2aebb0cb32 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java @@ -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 extends Compound super(FXCollections.observableArrayList()); } - + abstract Predicate getDuplicatePredicate(SubFilterType subfilter); + + public void addSubFilter(SubFilterType subfilter) { + addSubFilter(subfilter, Comparator.comparing(SubFilterType::getDisplayName)); + } + + protected void addSubFilter(SubFilterType subfilter, Comparator comparator) { + Predicate duplicatePredicate = getDuplicatePredicate(subfilter); + if (getSubFilters().stream().anyMatch(duplicatePredicate) == false) { + getSubFilters().add(subfilter); + } + getSubFilters().sort(comparator); + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCellFactory.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCellFactory.java index 689bb47702..4a2b7245cb 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCellFactory.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCellFactory.java @@ -38,13 +38,14 @@ class FilterCheckBoxCellFactory 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(); From b9a42f2c98207048e5f9fab9679ec230cd46d9fa Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 6 Apr 2016 11:07:24 -0400 Subject: [PATCH 28/35] minor cleanup and copyright fixes --- .../ui/detailview/DetailsChartLane.java | 52 ++++++++++++------- .../DetailsChartLayoutSettings.java | 19 +++++-- .../detailview/PrimaryDetailsChartLane.java | 2 +- .../ui/detailview/ScrollingLaneWrapper.java | 19 +++++-- .../ui/detailview/StripeFlattener.java | 2 +- 5 files changed, 66 insertions(+), 28 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java index f09d96fc6f..304c701b04 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java @@ -1,16 +1,31 @@ /* - * 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. + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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; @@ -76,7 +91,7 @@ abstract class DetailsChartLane extends XYChart activeQuickHidefilters; + private Set activeQuickHidefilters = new HashSet<>(); boolean quickHideFiltersEnabled() { return useQuickHideFilters; @@ -158,18 +173,15 @@ abstract class DetailsChartLane extends XYChart series = new Series<>(); - setData(FXCollections.observableArrayList()); - getData().add(series); + //add a dummy series or the chart is never rendered + setData(FXCollections.observableList(Arrays.asList(new Series()))); 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); +// 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); @@ -181,7 +193,7 @@ abstract class DetailsChartLane extends XYChart extends XYChart, Stream>> stripeFlattener = new Function, Stream>>() { - @Override - public Stream> apply(EventNodeBase node) { - return Stream.concat( - Stream.of(node), - node.getSubNodes().stream().flatMap(this::apply)); - } - }; + @Override + public Stream> apply(EventNodeBase node) { + return Stream.concat( + Stream.of(node), + node.getSubNodes().stream().flatMap(this::apply)); + } + }; return sortedNodes.stream() .flatMap(stripeFlattener) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLayoutSettings.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLayoutSettings.java index 76961b674e..412e9530f3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLayoutSettings.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLayoutSettings.java @@ -1,7 +1,20 @@ /* - * 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. + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java index 45aca19ff5..112baf0ebc 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/PrimaryDetailsChartLane.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingLaneWrapper.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingLaneWrapper.java index 67d569351b..35d006a081 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingLaneWrapper.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/ScrollingLaneWrapper.java @@ -1,7 +1,20 @@ /* - * 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. + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java index a2af78c4b4..b9b8b9a7e0 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/StripeFlattener.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); From 598100138b3b7df84f73777356a45f596a4c8256 Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 6 Apr 2016 11:17:57 -0400 Subject: [PATCH 29/35] fix bug where old settings node don't get cleared when the visualization changes --- .../autopsy/timeline/ui/AbstractVisualizationPane.java | 3 --- .../autopsy/timeline/ui/detailview/DetailViewPane.java | 3 ++- .../autopsy/timeline/ui/detailview/DetailsChartLane.java | 3 --- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index 74573a011e..b0c4ef85d1 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -134,9 +134,6 @@ public abstract class AbstractVisualizationPane> getDataSeries() { -// return FXCollections.unmodifiableObservableList(dataSeries); -// } /** * @return the list of nodes containing settings widgets to insert into this * visualization's header diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index af46818611..c8551a307d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -18,6 +18,7 @@ */ 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; @@ -95,7 +96,7 @@ public class DetailViewPane extends AbstractVisualizationPane(new DetailViewSettingsPane(chart.getLayoutSettings()).getChildrenUnmodifiable()); // //bind layout fo axes and spacers detailsChartDateAxis.getTickMarks().addListener((Observable observable) -> layoutDateLabels()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java index 304c701b04..d5bfc553d8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java @@ -179,9 +179,6 @@ abstract class DetailsChartLane extends XYChart Date: Thu, 7 Apr 2016 10:03:30 -0400 Subject: [PATCH 30/35] add hashCode and equals to EventCluster and EventStripe to prevent duplicate events getting pinned --- .../timeline/datamodel/EventCluster.java | 69 +++++++++++++++---- .../timeline/datamodel/EventStripe.java | 46 +++++++++++-- .../timeline/datamodel/TimeLineEvent.java | 1 - 3 files changed, 97 insertions(+), 19 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index 71a2d61482..c8f51247ba 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2013-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Sets; import java.util.Comparator; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.SortedSet; @@ -33,8 +34,8 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * 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. + * should have the same type and matching descriptions at the designated "zoom + * level", and be "close together" in time. */ @Immutable public class EventCluster implements MultiEvent { @@ -56,11 +57,16 @@ public class EventCluster implements MultiEvent { if (!cluster1.getDescription().equals(cluster2.getDescription())) { throw new IllegalArgumentException("event clusters are not compatible: they have different descriptions"); } - Sets.SetView idsUnion = Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs()); - Sets.SetView hashHitsUnion = Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits()); - Sets.SetView taggedUnion = Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags()); + Sets.SetView idsUnion = + Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs()); + Sets.SetView hashHitsUnion = + Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits()); + Sets.SetView 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; @@ -102,7 +108,9 @@ public class EventCluster implements MultiEvent { */ private final ImmutableSet hashHits; - private EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLoD lod, EventStripe parent) { + private EventCluster(Interval spanningInterval, EventType type, Set eventIDs, + Set hashHits, Set tagged, String description, DescriptionLoD lod, + EventStripe parent) { this.span = spanningInterval; this.type = type; @@ -114,7 +122,8 @@ public class EventCluster implements MultiEvent { this.parent = parent; } - public EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLoD lod) { + public EventCluster(Interval spanningInterval, EventType type, Set eventIDs, + Set hashHits, Set tagged, String description, DescriptionLoD lod) { this(spanningInterval, type, eventIDs, hashHits, tagged, description, lod, null); } @@ -138,19 +147,16 @@ public class EventCluster implements MultiEvent { } @Override - @SuppressWarnings("ReturnOfCollectionOrArrayField") public ImmutableSet getEventIDs() { return eventIDs; } @Override - @SuppressWarnings("ReturnOfCollectionOrArrayField") public ImmutableSet getEventIDsWithHashHits() { return hashHits; } @Override - @SuppressWarnings("ReturnOfCollectionOrArrayField") public ImmutableSet getEventIDsWithTags() { return tagged; } @@ -184,7 +190,7 @@ public class EventCluster implements MultiEvent { } @Override - public SortedSet< EventCluster> getClusters() { + public SortedSet getClusters() { return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(this).build(); } @@ -192,4 +198,41 @@ public class EventCluster implements MultiEvent { 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; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index bd160ac624..93c3799143 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -167,19 +167,16 @@ public final class EventStripe implements MultiEvent { } @Override - @SuppressWarnings("ReturnOfCollectionOrArrayField") public ImmutableSet getEventIDs() { return eventIDs; } @Override - @SuppressWarnings("ReturnOfCollectionOrArrayField") public ImmutableSet getEventIDsWithHashHits() { return hashHits; } @Override - @SuppressWarnings("ReturnOfCollectionOrArrayField") public ImmutableSet getEventIDsWithTags() { return tagged; } @@ -195,7 +192,6 @@ public final class EventStripe implements MultiEvent { } @Override - @SuppressWarnings("ReturnOfCollectionOrArrayField") public ImmutableSortedSet< EventCluster> getClusters() { return clusters; } @@ -205,4 +201,44 @@ public final class EventStripe implements MultiEvent { 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; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java index f0a2c53334..49aec1f00d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java @@ -48,6 +48,5 @@ public interface TimeLineEvent { return getEventIDs().size(); } - SortedSet getClusters(); } From 8bedda716a291c3d95dca28d05574fd586bf7e98 Mon Sep 17 00:00:00 2001 From: jmillman Date: Thu, 7 Apr 2016 11:07:05 -0400 Subject: [PATCH 31/35] strip redundant method declarations form MultiEvent interface --- .../timeline/datamodel/MultiEvent.java | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java index eee68eada8..92fde18b58 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/MultiEvent.java @@ -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 MultiEvent> extends TimeLineEvent { - String getDescription(); - - DescriptionLoD getDescriptionLoD(); - - Set getEventIDs(); - - Set getEventIDsWithHashHits(); - - Set getEventIDsWithTags(); - - EventType getEventType(); - long getEndMillis(); - long getStartMillis(); - Optional getParent(); - default int getSize() { - return getEventIDs().size(); - } - SortedSet getClusters(); } From 231a2da725c5d4fa0515c3501264de99b6a7b658 Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 11 Apr 2016 13:27:49 -0400 Subject: [PATCH 32/35] always "animeteTo" even if destination should match source dont do redundant sort --- .../timeline/ui/detailview/DetailsChartLane.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java index d5bfc553d8..54973f9329 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java @@ -121,7 +121,6 @@ abstract class DetailsChartLane extends XYChart extends XYChart::getEventType)).values() .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get()))); } else { - maxY.set(layoutEventBundleNodes(sortedNodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis)), 0)); + maxY.set(layoutEventBundleNodes(sortedNodes, 0)); } doAdditionalLayout(); setCursor(null); @@ -241,7 +241,6 @@ abstract class DetailsChartLane extends XYChart 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); @@ -262,10 +261,8 @@ abstract class DetailsChartLane extends XYChart Date: Tue, 12 Apr 2016 10:39:49 -0400 Subject: [PATCH 33/35] reinstate right click to close interval selector --- .../sleuthkit/autopsy/timeline/ui/IntervalSelector.java | 9 ++++++--- .../autopsy/timeline/ui/detailview/DetailViewPane.java | 4 ++-- .../autopsy/timeline/ui/detailview/DetailsChartLane.java | 6 ++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java index fe8635a31c..2ecddba51e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/IntervalSelector.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014-15 Basis Technology Corp. + * Copyright 2014-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,6 +32,7 @@ import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; 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; @@ -196,7 +197,9 @@ public abstract class IntervalSelector extends BorderPane { }); setOnMouseClicked(mouseClick -> { - if (mouseClick.getClickCount() >= 2) { + if (mouseClick.getButton() == MouseButton.SECONDARY) { + chart.clearIntervalSelector(); + } else if (mouseClick.getClickCount() >= 2) { zoomToSelectedInterval(); mouseClick.consume(); } @@ -248,7 +251,7 @@ public abstract class IntervalSelector 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}."}) + "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()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index c8551a307d..52ded83f90 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -87,11 +87,11 @@ public class DetailViewPane extends AbstractVisualizationPane verticalAxis = new EventAxis<>(Bundle.DetailViewPane_primaryLaneLabel_text()); - private final MappedList selectedEvents; + private final MappedList> selectedEvents; public DetailViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region bottomLeftSpacer) { super(controller, partPane, contextPane, bottomLeftSpacer); - this.selectedEvents = new MappedList<>(getSelectedNodes(), EventNodeBase::getEvent); + this.selectedEvents = new MappedList<>(getSelectedNodes(), EventNodeBase::getEvent); //initialize chart; chart = new DetailsChart(controller, detailsChartDateAxis, pinnedDateAxis, verticalAxis, getSelectedNodes()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java index 54973f9329..b1e67e7eca 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailsChartLane.java @@ -62,9 +62,11 @@ 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 null {@link #addDataItem(javafx.scene.chart.XYChart.Data) + * 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. */ @@ -268,6 +270,7 @@ abstract class DetailsChartLane extends XYChart extends XYChart Date: Tue, 12 Apr 2016 12:10:38 -0400 Subject: [PATCH 34/35] Lint of recentactivity/ExtractIE --- .../autopsy/recentactivity/ExtractIE.java | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java index 3ed41231f7..967fb4950c 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java @@ -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 42six 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 bbattributes = new ArrayList(); + Collection 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 bbattributes = new ArrayList(); + Collection bbattributes = new ArrayList<>(); bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL, NbBundle.getMessage(this.getClass(), "ExtractIE.parentModuleName.noSpace"), url)); @@ -378,12 +380,13 @@ class ExtractIE extends Extract { 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. + * 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. @@ -432,7 +435,7 @@ class ExtractIE extends Extract { } // Keep a list of reported user accounts to avoid repeats - Set reportedUserAccounts = new HashSet(); + Set reportedUserAccounts = new HashSet<>(); while (fileScanner.hasNext()) { String line = fileScanner.nextLine(); @@ -447,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 @@ -477,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.WARNING, "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 } } From 88f2a0d08ec16b92d01ed478d9a950c3d3e42b76 Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Tue, 12 Apr 2016 16:02:52 -0400 Subject: [PATCH 35/35] Update testing/RegressionTest for ingest job settings panel button label change --- .../src/org/sleuthkit/autopsy/testing/RegressionTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java b/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java index 1f081e06bd..99db8bfb85 100755 --- a/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java +++ b/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java @@ -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(); }