diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/History.java b/Core/src/org/sleuthkit/autopsy/coreutils/History.java index 50abf7e5cf..409cfe0006 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/History.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/History.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.coreutils; +import java.util.Deque; import java.util.Objects; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyBooleanProperty; @@ -143,7 +144,6 @@ public class History { * @throws IllegalArgumentException if newState == null */ synchronized public void advance(T newState) throws IllegalArgumentException { - if (newState != null && Objects.equals(currentState.get(), newState) == false) { if (currentState.get() != null) { historyStack.push(currentState.get()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index eebf0475bb..8367066591 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -76,10 +76,11 @@ import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.db.EventsRepository; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.SleuthkitCase; @@ -136,6 +137,13 @@ public class TimeLineController { private final Case autoCase; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private final ObservableList quickHideMaskFilters = FXCollections.observableArrayList(); + + public ObservableList getQuickHideFilters() { + return quickHideMaskFilters; + } + /** * @return the autopsy Case assigned to the controller */ @@ -173,7 +181,7 @@ public class TimeLineController { @GuardedBy("this") private final ReadOnlyObjectWrapper viewMode = new ReadOnlyObjectWrapper<>(VisualizationMode.COUNTS); - synchronized public ReadOnlyObjectProperty getViewMode() { + synchronized public ReadOnlyObjectProperty viewModeProperty() { return viewMode.getReadOnlyProperty(); } @@ -256,7 +264,7 @@ public class TimeLineController { InitialZoomState = new ZoomParams(filteredEvents.getSpanningInterval(), EventTypeZoomLevel.BASE_TYPE, filteredEvents.filterProperty().get(), - DescriptionLOD.SHORT); + DescriptionLoD.SHORT); historyManager.advance(InitialZoomState); } @@ -556,12 +564,12 @@ public class TimeLineController { @NbBundle.Messages({"# {0} - the number of events", "Timeline.pushDescrLOD.confdlg.msg=You are about to show details for {0} events." + " This might be very slow or even crash Autopsy.\n\nDo you want to continue?"}) - synchronized public boolean pushDescrLOD(DescriptionLOD newLOD) { + synchronized public boolean pushDescrLOD(DescriptionLoD newLOD) { Map eventCounts = filteredEvents.getEventCounts(filteredEvents.zoomParametersProperty().get().getTimeRange()); final Long count = eventCounts.values().stream().reduce(0l, Long::sum); boolean shouldContinue = true; - if (newLOD == DescriptionLOD.FULL && count > 10_000) { + if (newLOD == DescriptionLoD.FULL && count > 10_000) { String format = NumberFormat.getInstance().format(count); int showConfirmDialog = JOptionPane.showConfirmDialog(mainFrame, @@ -616,7 +624,6 @@ public class TimeLineController { synchronized private void advance(ZoomParams newState) { historyManager.advance(newState); - } public void selectTimeAndType(Interval interval, EventType type) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java index 33e3f63ac1..f19dc9bf1c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java @@ -169,12 +169,12 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer new Forward(controller).handle(new ActionEvent()); } }); - controller.getViewMode().addListener((Observable observable) -> { - if (controller.getViewMode().get().equals(VisualizationMode.COUNTS)) { + controller.viewModeProperty().addListener((Observable observable) -> { + if (controller.viewModeProperty().get().equals(VisualizationMode.COUNTS)) { tabPane.getSelectionModel().select(filterTab); } }); - eventsTab.disableProperty().bind(controller.getViewMode().isEqualTo(VisualizationMode.COUNTS)); + eventsTab.disableProperty().bind(controller.viewModeProperty().isEqualTo(VisualizationMode.COUNTS)); visualizationPanel.setController(controller); navPanel.setController(controller); filtersPanel.setController(controller); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java index 17d5a01e7b..8714452725 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java @@ -1,14 +1,28 @@ /* - * 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 2015 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 com.google.common.collect.Range; +import java.util.Optional; import java.util.Set; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * @@ -17,10 +31,11 @@ public interface EventBundle { String getDescription(); - DescriptionLOD getDescriptionLOD(); + DescriptionLoD getDescriptionLoD(); Set getEventIDs(); + Set getEventIDsWithHashHits(); Set getEventIDsWithTags(); @@ -33,4 +48,9 @@ public interface EventBundle { Iterable> getRanges(); + Optional getParentBundle(); + + default long getCount() { + return getEventIDs().size(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index 7533c486e1..57d5cec0db 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -21,59 +21,87 @@ package org.sleuthkit.autopsy.timeline.datamodel; import com.google.common.collect.Range; import com.google.common.collect.Sets; import java.util.Collections; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import javax.annotation.concurrent.Immutable; import org.joda.time.Interval; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** - * Represents a set of other (TimeLineEvent) events aggregated together. All the + * Represents a set of other (TimeLineEvent) events clustered together. All the * sub events should have the same type and matching descriptions at the - * designated 'zoom level'. + * designated 'zoom level', and be 'close together' in time. */ @Immutable public class EventCluster implements EventBundle { /** - * the smallest time interval containing all the aggregated events + * merge two event clusters into one new event cluster. + * + * @param cluster1 + * @param cluster2 + * + * @return a new event cluster that is the result of merging the given + * events clusters + */ + public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) { + if (cluster1.getEventType() != cluster2.getEventType()) { + throw new IllegalArgumentException("event clusters are not compatible: they have different types"); + } + + 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()); + + return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod); + } + + final private EventBundle parent; + + /** + * the smallest time interval containing all the clustered events */ final private Interval span; /** - * the type of all the aggregted events + * the type of all the clustered events */ final private EventType type; /** - * the common description of all the aggregated events + * the common description of all the clustered events */ final private String description; /** - * the description level of detail that the events were aggregated at. + * the description level of detail that the events were clustered at. */ - private final DescriptionLOD lod; + private final DescriptionLoD lod; /** - * the set of ids of the aggregated events + * the set of ids of the clustered events */ final private Set eventIDs; /** - * the ids of the subset of aggregated events that have at least one tag + * the ids of the subset of clustered events that have at least one tag * applied to them */ private final Set tagged; /** - * the ids of the subset of aggregated events that have at least one hash - * set hit + * the ids of the subset of clustered events that have at least one hash set + * hit */ private final Set hashHits; - public EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLOD lod) { + private EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLoD lod, EventBundle parent) { this.span = spanningInterval; this.type = type; @@ -82,73 +110,62 @@ public class EventCluster implements EventBundle { this.description = description; this.eventIDs = eventIDs; this.lod = lod; + this.parent = parent; + } + + 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); + } + + @Override + public Optional getParentBundle() { + return Optional.ofNullable(parent); } - /** - * @return the actual interval from the first event to the last event - */ public Interval getSpan() { return span; } + @Override public long getStartMillis() { return span.getStartMillis(); } + @Override public long getEndMillis() { return span.getEndMillis(); } + @Override public Set getEventIDs() { return Collections.unmodifiableSet(eventIDs); } + @Override public Set getEventIDsWithHashHits() { return Collections.unmodifiableSet(hashHits); } + @Override public Set getEventIDsWithTags() { return Collections.unmodifiableSet(tagged); } + @Override public String getDescription() { return description; } + @Override public EventType getEventType() { return type; } @Override - public DescriptionLOD getDescriptionLOD() { + public DescriptionLoD getDescriptionLoD() { return lod; } - /** - * merge two aggregate events into one new aggregate event. - * - * @param cluster1 - * @param aggEVent2 - * - * @return a new aggregate event that is the result of merging the given - * events - */ - public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) { - - if (cluster1.getEventType() != cluster2.getEventType()) { - throw new IllegalArgumentException("aggregate events are not compatible they have different types"); - } - - if (!cluster1.getDescription().equals(cluster2.getDescription())) { - throw new IllegalArgumentException("aggregate events 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()); - - return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod); - } - Range getRange() { if (getEndMillis() > getStartMillis()) { return Range.closedOpen(getSpan().getStartMillis(), getSpan().getEndMillis()); @@ -162,4 +179,20 @@ public class EventCluster implements EventBundle { return Collections.singletonList(getRange()); } + /** + * return a new EventCluster identical to this one, except with the given + * EventBundle as the parent. + * + * @param parent + * + * @return a new EventCluster identical to this one, except with the given + * EventBundle as the parent. + */ + public EventCluster withParent(EventBundle 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); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index e88e8ff623..8a465de1cd 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -13,50 +13,63 @@ import com.google.common.collect.TreeRangeMap; import com.google.common.collect.TreeRangeSet; import java.util.Collections; import java.util.HashSet; +import java.util.Optional; import java.util.Set; 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; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** - * + * A 'collection' of {@link EventCluster}s, all having the same type, + * description, and zoom levels. */ @Immutable public final class EventStripe implements EventBundle { + 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)); + return new EventStripe(u, v); + } + + private final EventBundle parent; + private final RangeSet spans = TreeRangeSet.create(); private final RangeMap spanMap = TreeRangeMap.create(); /** - * the type of all the aggregted events + * the type of all the events */ private final EventType type; /** - * the common description of all the aggregated events + * the common description of all the events */ private final String description; /** - * the description level of detail that the events were aggregated at. + * the description level of detail that the events were clustered at. */ - private final DescriptionLOD lod; + private final DescriptionLoD lod; /** - * the set of ids of the aggregated events + * the set of ids of the events */ private final Set eventIDs = new HashSet<>(); /** - * the ids of the subset of aggregated events that have at least one tag - * applied to them + * the ids of the subset of events that have at least one tag applied to + * them */ private final Set tagged = new HashSet<>(); /** - * the ids of the subset of aggregated events that have at least one hash - * set hit + * the ids of the subset of events that have at least one hash set hit */ private final Set hashHits = new HashSet<>(); @@ -65,10 +78,11 @@ public final class EventStripe implements EventBundle { spanMap.put(cluster.getRange(), cluster); type = cluster.getEventType(); description = cluster.getDescription(); - lod = cluster.getDescriptionLOD(); + lod = cluster.getDescriptionLoD(); eventIDs.addAll(cluster.getEventIDs()); tagged.addAll(cluster.getEventIDsWithTags()); hashHits.addAll(cluster.getEventIDsWithHashHits()); + parent = cluster.getParentBundle().orElse(null); } private EventStripe(EventStripe u, EventStripe v) { @@ -78,22 +92,19 @@ public final class EventStripe implements EventBundle { spanMap.putAll(v.spanMap); type = u.getEventType(); description = u.getDescription(); - lod = u.getDescriptionLOD(); + lod = u.getDescriptionLoD(); eventIDs.addAll(u.getEventIDs()); eventIDs.addAll(v.getEventIDs()); tagged.addAll(u.getEventIDsWithTags()); tagged.addAll(v.getEventIDsWithTags()); hashHits.addAll(u.getEventIDsWithHashHits()); hashHits.addAll(v.getEventIDsWithHashHits()); + parent = u.getParentBundle().orElse(null); } - 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)); - return new EventStripe(u, v); + @Override + public Optional getParentBundle() { + return Optional.ofNullable(parent); } @Override @@ -107,7 +118,7 @@ public final class EventStripe implements EventBundle { } @Override - public DescriptionLOD getDescriptionLOD() { + public DescriptionLoD getDescriptionLoD() { return lod; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index 664ae74ddd..6da5fea635 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.timeline.datamodel; import com.google.common.eventbus.EventBus; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -55,7 +56,7 @@ import org.sleuthkit.autopsy.timeline.filters.TagNameFilter; import org.sleuthkit.autopsy.timeline.filters.TagsFilter; import org.sleuthkit.autopsy.timeline.filters.TextFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -104,7 +105,7 @@ public final class FilteredEventsModel { private final ReadOnlyObjectWrapper< EventTypeZoomLevel> requestedTypeZoom = new ReadOnlyObjectWrapper<>(EventTypeZoomLevel.BASE_TYPE); @GuardedBy("this") - private final ReadOnlyObjectWrapper< DescriptionLOD> requestedLOD = new ReadOnlyObjectWrapper<>(DescriptionLOD.SHORT); + private final ReadOnlyObjectWrapper< DescriptionLoD> requestedLOD = new ReadOnlyObjectWrapper<>(DescriptionLoD.SHORT); @GuardedBy("this") private final ReadOnlyObjectWrapper requestedZoomParamters = new ReadOnlyObjectWrapper<>(); @@ -143,6 +144,7 @@ public final class FilteredEventsModel { }); requestedFilter.set(getDefaultFilter()); + //TODO: use bindings to keep these in sync? -jm requestedZoomParamters.addListener((Observable observable) -> { final ZoomParams zoomParams = requestedZoomParamters.get(); @@ -153,7 +155,7 @@ public final class FilteredEventsModel { || zoomParams.getTimeRange().equals(requestedTimeRange.get()) == false) { requestedTypeZoom.set(zoomParams.getTypeZoomLevel()); - requestedFilter.set(zoomParams.getFilter().copyOf()); + requestedFilter.set(zoomParams.getFilter()); requestedTimeRange.set(zoomParams.getTimeRange()); requestedLOD.set(zoomParams.getDescriptionLOD()); } @@ -178,7 +180,7 @@ public final class FilteredEventsModel { return requestedTimeRange.getReadOnlyProperty(); } - synchronized public ReadOnlyObjectProperty descriptionLODProperty() { + synchronized public ReadOnlyObjectProperty descriptionLODProperty() { return requestedLOD.getReadOnlyProperty(); } @@ -190,7 +192,7 @@ public final class FilteredEventsModel { return requestedTypeZoom.getReadOnlyProperty(); } - synchronized public DescriptionLOD getDescriptionLOD() { + synchronized public DescriptionLoD getDescriptionLOD() { return requestedLOD.get(); } @@ -227,7 +229,7 @@ public final class FilteredEventsModel { tagNameFilter.setSelected(Boolean.TRUE); tagsFilter.addSubFilter(tagNameFilter); }); - return new RootFilter(new HideKnownFilter(), tagsFilter, hashHitsFilter, new TextFilter(), new TypeFilter(RootEventType.getInstance()), dataSourcesFilter); + return new RootFilter(new HideKnownFilter(), tagsFilter, hashHitsFilter, new TextFilter(), new TypeFilter(RootEventType.getInstance()), dataSourcesFilter, Collections.emptySet()); } public Interval getBoundingEventsInterval() { @@ -320,17 +322,15 @@ public final class FilteredEventsModel { } /** - * @param aggregation * - * @return a list of aggregated events that are within the requested time - * range and pass the requested filter, using the given aggregation - * to control the grouping of events + * @return a list of event clusters at the requested zoom levels that are + * within the requested time range and pass the requested filter */ - public List getAggregatedEvents() { + public List getEventClusters() { final Interval range; final RootFilter filter; final EventTypeZoomLevel zoom; - final DescriptionLOD lod; + final DescriptionLoD lod; synchronized (this) { range = requestedTimeRange.get(); filter = requestedFilter.get(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java index 744311c321..2296edfbf8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java @@ -22,7 +22,7 @@ import com.google.common.collect.ImmutableMap; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.datamodel.TskData; /** @@ -38,7 +38,7 @@ public class TimeLineEvent { private final long time; private final EventType subType; - private final ImmutableMap descriptions; + private final ImmutableMap descriptions; private final TskData.FileKnown known; private final boolean hashHit; @@ -50,10 +50,9 @@ public class TimeLineEvent { this.artifactID = artifactID == 0 ? null : artifactID; this.time = time; this.subType = type; - descriptions = ImmutableMap.of( - DescriptionLOD.FULL, fullDescription, - DescriptionLOD.MEDIUM, medDescription, - DescriptionLOD.SHORT, shortDescription); + descriptions = ImmutableMap.of(DescriptionLoD.FULL, fullDescription, + DescriptionLoD.MEDIUM, medDescription, + DescriptionLoD.SHORT, shortDescription); this.known = known; this.hashHit = hashHit; @@ -94,22 +93,22 @@ public class TimeLineEvent { } public String getFullDescription() { - return getDescription(DescriptionLOD.FULL); + return getDescription(DescriptionLoD.FULL); } public String getMedDescription() { - return getDescription(DescriptionLOD.MEDIUM); + return getDescription(DescriptionLoD.MEDIUM); } public String getShortDescription() { - return getDescription(DescriptionLOD.SHORT); + return getDescription(DescriptionLoD.SHORT); } public TskData.FileKnown getKnown() { return known; } - public String getDescription(DescriptionLOD lod) { + public String getDescription(DescriptionLoD lod) { return descriptions.get(lod); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index 6b19122c8d..ce248a2149 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java @@ -64,7 +64,7 @@ import static org.sleuthkit.autopsy.timeline.db.SQLHelper.useTagTablesHelper; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TagsFilter; import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.SleuthkitCase; @@ -1046,7 +1046,7 @@ public class EventDB { //unpack params Interval timeRange = params.getTimeRange(); RootFilter filter = params.getFilter(); - DescriptionLOD descriptionLOD = params.getDescriptionLOD(); + DescriptionLoD descriptionLOD = params.getDescriptionLOD(); EventTypeZoomLevel typeZoomLevel = params.getTypeZoomLevel(); //ensure length of querried interval is not 0 @@ -1076,6 +1076,7 @@ public class EventDB { + "\n GROUP BY interval, " + typeColumn + " , " + descriptionColumn // NON-NLS + "\n ORDER BY min(time)"; // NON-NLS + System.out.println(query); // perform query and map results to AggregateEvent objects List events = new ArrayList<>(); @@ -1108,7 +1109,7 @@ public class EventDB { * * @throws SQLException */ - private EventCluster eventClusterHelper(ResultSet rs, boolean useSubTypes, DescriptionLOD descriptionLOD, TagsFilter filter) throws SQLException { + private EventCluster eventClusterHelper(ResultSet rs, boolean useSubTypes, DescriptionLoD descriptionLOD, TagsFilter filter) throws SQLException { Interval interval = new Interval(rs.getLong("min(time)") * 1000, rs.getLong("max(time)") * 1000, TimeLineController.getJodaTimeZone());// NON-NLS String eventIDsString = rs.getString("event_ids");// NON-NLS Set eventIDs = SQLHelper.unGroupConcat(eventIDsString, Long::valueOf); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index c2a03545a6..c2eb36b7f6 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -622,4 +622,9 @@ public class EventsRepository { } } } + + public boolean areFiltersEquivalent(RootFilter f1, RootFilter f2) { + return SQLHelper.getSQLWhere(f1).equals(SQLHelper.getSQLWhere(f2)); + + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java b/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java index 9b25bd401b..65405d7930 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java @@ -43,9 +43,9 @@ import org.sleuthkit.autopsy.timeline.filters.TextFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.filters.UnionFilter; import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; -import static org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD.FULL; -import static org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD.MEDIUM; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; +import static org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD.FULL; +import static org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD.MEDIUM; import static org.sleuthkit.autopsy.timeline.zooming.TimeUnits.DAYS; import static org.sleuthkit.autopsy.timeline.zooming.TimeUnits.HOURS; import static org.sleuthkit.autopsy.timeline.zooming.TimeUnits.MINUTES; @@ -58,16 +58,16 @@ import org.sleuthkit.datamodel.TskData; * Static helper methods for converting between java data model objects and * sqlite queries. */ -public class SQLHelper { +class SQLHelper { static String useHashHitTablesHelper(RootFilter filter) { HashHitsFilter hashHitFilter = filter.getHashHitsFilter(); - return hashHitFilter.isSelected() && false == hashHitFilter.isDisabled() ? " LEFT JOIN hash_set_hits " : " "; + return hashHitFilter.isActive() ? " LEFT JOIN hash_set_hits " : " "; } static String useTagTablesHelper(RootFilter filter) { TagsFilter tagsFilter = filter.getTagsFilter(); - return tagsFilter.isSelected() && false == tagsFilter.isDisabled() ? " LEFT JOIN tags " : " "; + return tagsFilter.isActive() ? " LEFT JOIN tags " : " "; } /** @@ -149,7 +149,7 @@ public class SQLHelper { } private static String getSQLWhere(HideKnownFilter filter) { - if (filter.isSelected()) { + if (filter.isActive()) { return "(known_state IS NOT '" + TskData.FileKnown.KNOWN.getFileKnownValue() + "')"; // NON-NLS } else { return "1"; @@ -157,16 +157,16 @@ public class SQLHelper { } private static String getSQLWhere(DescriptionFilter filter) { - if (filter.isSelected()) { - return "(" + getDescriptionColumn(filter.getDescriptionLoD()) + " LIKE '" + filter.getDescription() + "')"; // NON-NLS + if (filter.isActive()) { + String likeOrNotLike = (filter.getFilterMode() == DescriptionFilter.FilterMode.INCLUDE ? "" : " NOT") + " LIKE '"; + return "(" + getDescriptionColumn(filter.getDescriptionLoD()) + likeOrNotLike + filter.getDescription() + "' )"; // NON-NLS } else { return "1"; } } private static String getSQLWhere(TagsFilter filter) { - if (filter.isSelected() - && (false == filter.isDisabled()) + if (filter.isActive() && (filter.getSubFilters().isEmpty() == false)) { String tagNameIDs = filter.getSubFilters().stream() .filter((TagNameFilter t) -> t.isSelected() && !t.isDisabled()) @@ -181,8 +181,7 @@ public class SQLHelper { } private static String getSQLWhere(HashHitsFilter filter) { - if (filter.isSelected() - && (false == filter.isDisabled()) + if (filter.isActive() && (filter.getSubFilters().isEmpty() == false)) { String hashSetIDs = filter.getSubFilters().stream() .filter((HashSetFilter t) -> t.isSelected() && !t.isDisabled()) @@ -195,7 +194,7 @@ public class SQLHelper { } private static String getSQLWhere(DataSourceFilter filter) { - if (filter.isSelected()) { + if (filter.isActive()) { return "(datasource_id = '" + filter.getDataSourceID() + "')"; } else { return "1"; @@ -203,15 +202,15 @@ public class SQLHelper { } private static String getSQLWhere(DataSourcesFilter filter) { - return (filter.isSelected()) ? "(datasource_id in (" + return (filter.isActive()) ? "(datasource_id in (" + filter.getSubFilters().stream() - .filter(AbstractFilter::isSelected) + .filter(AbstractFilter::isActive) .map((dataSourceFilter) -> String.valueOf(dataSourceFilter.getDataSourceID())) .collect(Collectors.joining(", ")) + "))" : "1"; } private static String getSQLWhere(TextFilter filter) { - if (filter.isSelected()) { + if (filter.isActive()) { if (StringUtils.isBlank(filter.getText())) { return "1"; } @@ -237,7 +236,7 @@ public class SQLHelper { return "0"; } else if (typeFilter.getEventType() instanceof RootEventType) { if (typeFilter.getSubFilters().stream() - .allMatch(subFilter -> subFilter.isSelected() && subFilter.getSubFilters().stream().allMatch(Filter::isSelected))) { + .allMatch(subFilter -> subFilter.isActive() && subFilter.getSubFilters().stream().allMatch(Filter::isActive))) { return "1"; //then collapse clause to true } } @@ -245,7 +244,7 @@ public class SQLHelper { } private static List getActiveSubTypes(TypeFilter filter) { - if (filter.isSelected()) { + if (filter.isActive()) { if (filter.getSubFilters().isEmpty()) { return Collections.singletonList(RootEventType.allTypes.indexOf(filter.getEventType())); } else { @@ -285,7 +284,7 @@ public class SQLHelper { } } - static String getDescriptionColumn(DescriptionLOD lod) { + static String getDescriptionColumn(DescriptionLoD lod) { switch (lod) { case FULL: return "full_description"; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java index 3411ade930..c5871454d8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.timeline.filters; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; import javafx.beans.property.SimpleBooleanProperty; /** @@ -28,9 +30,10 @@ public abstract class AbstractFilter implements Filter { private final SimpleBooleanProperty selected = new SimpleBooleanProperty(true); private final SimpleBooleanProperty disabled = new SimpleBooleanProperty(false); + private final BooleanBinding activeProperty = Bindings.and(selected, disabled.not()); @Override - public SimpleBooleanProperty getSelectedProperty() { + public SimpleBooleanProperty selectedProperty() { return selected; } @@ -64,4 +67,11 @@ public abstract class AbstractFilter implements Filter { return "[" + (isSelected() ? "x" : " ") + "]"; // NON-NLS } + public final boolean isActive() { + return activeProperty.get(); + } + + public final BooleanBinding activeProperty() { + return activeProperty; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java index 297b0a3a1b..bf4c6a1b07 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.timeline.filters; import java.util.List; +import java.util.Objects; import javafx.beans.Observable; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; @@ -71,7 +72,7 @@ public abstract class CompoundFilter extends Abstr private void addSubFilterListeners(List newSubfilters) { for (SubFilterType sf : newSubfilters) { //if a subfilter changes active state - sf.getSelectedProperty().addListener((Observable observable) -> { + sf.selectedProperty().addListener((Observable observable) -> { //set this filter acttive af any of the subfilters are active. setSelected(getSubFilters().parallelStream().anyMatch(Filter::isSelected)); }); @@ -83,10 +84,21 @@ public abstract class CompoundFilter extends Abstr return false; } for (int i = 0; i < oneFilter.getSubFilters().size(); i++) { - if (oneFilter.getSubFilters().get(i).equals(otherFilter.getSubFilters().get(i)) == false) { + final SubFilterType subFilter = oneFilter.getSubFilters().get(i); + final SubFilterType otherSubFilter = otherFilter.getSubFilters().get(i); + if (subFilter.equals(otherSubFilter) == false + || subFilter.isDisabled() != otherSubFilter.isDisabled() + || subFilter.isSelected() != otherSubFilter.isSelected()) { return false; } } return true; } + + @Override + public int hashCode() { + int hash = 3; + hash = 61 * hash + Objects.hashCode(this.subFilters); + return hash; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java index b75193ddeb..2cd3f93d9e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java @@ -84,6 +84,4 @@ public class DataSourceFilter extends AbstractFilter { } return isSelected() == other.isSelected(); } - - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java index 8ee273f50a..5ac569c6f9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java @@ -1,26 +1,45 @@ /* - * 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 2015 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.filters; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import java.util.Objects; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; public class DescriptionFilter extends AbstractFilter { - private final DescriptionLOD descriptionLoD; - + private final DescriptionLoD descriptionLoD; private final String description; + private final FilterMode filterMode; - public DescriptionFilter(DescriptionLOD descriptionLoD, String description) { + public FilterMode getFilterMode() { + return filterMode; + } + + public DescriptionFilter(DescriptionLoD descriptionLoD, String description, FilterMode filterMode) { this.descriptionLoD = descriptionLoD; this.description = description; + this.filterMode = filterMode; } @Override public DescriptionFilter copyOf() { - DescriptionFilter filterCopy = new DescriptionFilter(getDescriptionLoD(), getDescription()); + DescriptionFilter filterCopy = new DescriptionFilter(getDescriptionLoD(), getDescription(), getFilterMode()); filterCopy.setSelected(isSelected()); filterCopy.setDisabled(isDisabled()); return filterCopy; @@ -28,18 +47,18 @@ public class DescriptionFilter extends AbstractFilter { @Override public String getDisplayName() { - return "description"; + return getDescriptionLoD().getDisplayName() + ": " + getDescription(); } @Override public String getHTMLReportString() { - return getDescriptionLoD().getDisplayName() + " " + getDisplayName() + " = " + getDescription(); + return getDisplayName() + getStringCheckBox(); } /** * @return the descriptionLoD */ - public DescriptionLOD getDescriptionLoD() { + public DescriptionLoD getDescriptionLoD() { return descriptionLoD; } @@ -50,4 +69,49 @@ public class DescriptionFilter extends AbstractFilter { return description; } + public enum FilterMode { + + EXCLUDE("Exclude"), + INCLUDE("Include"); + + private final String displayName; + + private FilterMode(String displayName) { + this.displayName = displayName; + } + + private String getDisplayName() { + return displayName; + } + } + + @Override + public int hashCode() { + int hash = 7; + hash = 79 * hash + Objects.hashCode(this.descriptionLoD); + hash = 79 * hash + Objects.hashCode(this.description); + hash = 79 * hash + Objects.hashCode(this.filterMode); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DescriptionFilter other = (DescriptionFilter) obj; + if (this.descriptionLoD != other.descriptionLoD) { + return false; + } + if (!Objects.equals(this.description, other.description)) { + return false; + } + if (this.filterMode != other.filterMode) { + return false; + } + return true; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java index 709bf116d4..dc44c8912b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.timeline.filters; +import javafx.beans.binding.BooleanBinding; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -66,7 +67,7 @@ public interface Filter { void setSelected(Boolean act); - SimpleBooleanProperty getSelectedProperty(); + SimpleBooleanProperty selectedProperty(); /* * TODO: disabled state only affects the state of the checkboxes in the ui @@ -79,6 +80,8 @@ public interface Filter { SimpleBooleanProperty getDisabledProperty(); boolean isDisabled(); - - + + boolean isActive(); + + BooleanBinding activeProperty(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java index b47c4d481f..f5e5b30731 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java @@ -33,7 +33,7 @@ public class HideKnownFilter extends AbstractFilter { public HideKnownFilter() { super(); - getSelectedProperty().set(false); + selectedProperty().set(false); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java index f99249dcf0..7295a066b2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java @@ -88,10 +88,4 @@ public class IntersectionFilter extends CompoundFilter { } return true; } - - @Override - public int hashCode() { - int hash = 7; - return hash; - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java index bd115f303b..9034810ed7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.timeline.filters; +import java.util.Set; +import java.util.stream.Collectors; import javafx.collections.FXCollections; /** @@ -45,21 +47,46 @@ public class RootFilter extends IntersectionFilter { return hashFilter; } - public RootFilter(HideKnownFilter knownFilter, TagsFilter tagsFilter, HashHitsFilter hashFilter, TextFilter textFilter, TypeFilter typeFilter, DataSourcesFilter dataSourceFilter) { - super(FXCollections.observableArrayList(knownFilter, tagsFilter, hashFilter, textFilter, dataSourceFilter, typeFilter)); - setSelected(Boolean.TRUE); - setDisabled(false); + public RootFilter(HideKnownFilter knownFilter, TagsFilter tagsFilter, HashHitsFilter hashFilter, TextFilter textFilter, TypeFilter typeFilter, DataSourcesFilter dataSourceFilter, Set annonymousSubFilters) { + super(FXCollections.observableArrayList( + textFilter, + knownFilter, + dataSourceFilter, tagsFilter, + hashFilter, + typeFilter + )); this.knownFilter = knownFilter; this.tagsFilter = tagsFilter; this.hashFilter = hashFilter; this.textFilter = textFilter; this.typeFilter = typeFilter; this.dataSourcesFilter = dataSourceFilter; + getSubFilters().addAll(annonymousSubFilters); + setSelected(Boolean.TRUE); + setDisabled(false); } @Override public RootFilter copyOf() { - RootFilter filter = new RootFilter(knownFilter.copyOf(), tagsFilter.copyOf(), hashFilter.copyOf(), textFilter.copyOf(), typeFilter.copyOf(), dataSourcesFilter.copyOf()); + Set annonymousSubFilters = getSubFilters().stream() + .filter(subFilter -> + !(subFilter.equals(knownFilter) + || subFilter.equals(tagsFilter) + || subFilter.equals(hashFilter) + || subFilter.equals(typeFilter) + || subFilter.equals(textFilter) + || subFilter.equals(dataSourcesFilter))) + .map(Filter::copyOf) + .collect(Collectors.toSet()); + + RootFilter filter = new RootFilter( + knownFilter.copyOf(), + tagsFilter.copyOf(), + hashFilter.copyOf(), + textFilter.copyOf(), + typeFilter.copyOf(), + dataSourcesFilter.copyOf(), + annonymousSubFilters); filter.setSelected(isSelected()); filter.setDisabled(isDisabled()); return filter; @@ -67,7 +94,7 @@ public class RootFilter extends IntersectionFilter { @Override public int hashCode() { - return 3; + return super.hashCode(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java index e4a287f03d..07ef904e75 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java @@ -33,4 +33,6 @@ abstract public class UnionFilter extends Compound public UnionFilter() { super(FXCollections.observableArrayList()); } + + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/eye--minus.png b/Core/src/org/sleuthkit/autopsy/timeline/images/eye--minus.png new file mode 100644 index 0000000000..08b048eae3 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/eye--minus.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/eye--plus.png b/Core/src/org/sleuthkit/autopsy/timeline/images/eye--plus.png new file mode 100644 index 0000000000..4ad653156f Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/eye--plus.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractFXCellFactory.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractFXCellFactory.java new file mode 100644 index 0000000000..dadc14f121 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractFXCellFactory.java @@ -0,0 +1,80 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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; + +import java.util.function.Supplier; +import javafx.scene.control.IndexedCell; +import javafx.scene.control.ListCell; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TreeTableCell; +import javafx.scene.control.TreeTableColumn; + +/** + * an abstract base class for Cell factories. This class provides the basic + * infrustructure for implementations to be able to create similar cells for + * listview, tableviews or treetableviews via the appropriate method call. + * Implementations need only implement the abstract configureCell method in the + * same spirit as IndexedCell.updateItem + */ +public abstract class AbstractFXCellFactory { + + public TreeTableCell< X, Y> forTreeTable(TreeTableColumn< X, Y> column) { + return new AbstractTreeTableCell(); + } + + public TableCell forTable(TableColumn column) { + return new AbstractTableCell(); + } + + public ListCell< Y> forList() { + return new AbstractListCell(); + } + + protected abstract void configureCell(IndexedCell cell, Y item, boolean empty, Supplier supplier); + + private class AbstractTableCell extends TableCell { + + @Override + @SuppressWarnings({"unchecked"}) //we know it will be X but there is a flaw in getTableRow return type + protected void updateItem(Y item, boolean empty) { + super.updateItem(item, empty); + configureCell(this, item, empty, (() -> (X) this.getTableRow().getItem())); + } + } + + private class AbstractTreeTableCell extends TreeTableCell { + + @Override + protected void updateItem(Y item, boolean empty) { + super.updateItem(item, empty); + configureCell(this, item, empty, (() -> this.getTreeTableRow().getItem())); + } + } + + private class AbstractListCell extends ListCell< Y> { + + @Override + @SuppressWarnings("unchecked") //for a list X should always equal Y + protected void updateItem(Y item, boolean empty) { + super.updateItem(item, empty); + configureCell(this, item, empty, () -> (X) this.getItem()); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java index 6d6fb8fb2e..993f61501e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java @@ -304,7 +304,7 @@ public abstract class AbstractVisualization & T //x-positions (pixels) of the current branch and leaf labels double leafLabelX = 0; - if (dateTime.branch.equals("")) { + if (dateTime.branch.isEmpty()) { //if there is only one part to the date (ie only year), just add a label for each tick for (Axis.TickMark t : tickMarks) { assignLeafLabel(new TwoPartDateTime(getTickMarkLabel(t.getValue())).leaf, diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java index 6d601e06a8..6aca0b6b6c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java @@ -315,14 +315,14 @@ public class VisualizationPanel extends BorderPane implements TimeLineView { public synchronized void setController(TimeLineController controller) { this.controller = controller; setModel(controller.getEventsModel()); - setViewMode(controller.getViewMode().get()); + setViewMode(controller.viewModeProperty().get()); controller.getNeedsHistogramRebuild().addListener((ObservableValue observable, Boolean oldValue, Boolean newValue) -> { if (newValue) { refreshHistorgram(); } }); - controller.getViewMode().addListener((ObservableValue ov, VisualizationMode t, VisualizationMode t1) -> { + controller.viewModeProperty().addListener((ObservableValue ov, VisualizationMode t, VisualizationMode t1) -> { setViewMode(t1); }); TimeLineController.getTimeZone().addListener(timeRangeInvalidationListener); @@ -361,7 +361,7 @@ public class VisualizationPanel extends BorderPane implements TimeLineView { } } - private synchronized void setVisualization(final AbstractVisualization newViz) { + private synchronized void setVisualization(final AbstractVisualization newViz) { Platform.runLater(() -> { synchronized (VisualizationPanel.this) { if (visualization != null) { @@ -375,7 +375,7 @@ public class VisualizationPanel extends BorderPane implements TimeLineView { visualization.setController(controller); notificationPane.setContent(visualization); if (visualization instanceof DetailViewPane) { - navPanel.setChart((DetailViewPane) visualization); + navPanel.setDetailViewPane((DetailViewPane) visualization); } visualization.hasEvents.addListener((ObservableValue observable, Boolean oldValue, Boolean newValue) -> { if (newValue == false) { 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 46d6709285..517cddde4e 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 Basis Technology Corp. + * Copyright 2014-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,10 +18,9 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import java.net.URL; import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.ResourceBundle; import java.util.concurrent.ConcurrentHashMap; import javafx.application.Platform; import javafx.beans.InvalidationListener; @@ -62,6 +61,7 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import org.controlsfx.control.action.Action; import org.joda.time.DateTime; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.LoggedTask; @@ -69,41 +69,31 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualization; -import org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane; -import org.sleuthkit.autopsy.timeline.ui.detailview.tree.NavTreeNode; import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** - * FXML Controller class for a {@link EventDetailChart} based implementation of - * a TimeLineView. + * Controller class for a {@link EventDetailChart} based implementation of a + * TimeLineView. * * This class listens to changes in the assigned {@link FilteredEventsModel} and * updates the internal {@link EventDetailChart} to reflect the currently * requested events. * - * This class captures input from the user in the form of mouse clicks on graph - * bars, and forwards them to the assigned {@link TimeLineController} - * * 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 (through {@link Platform#runLater(java.lang.Runnable) - * } - * - * {@link CountsChartPane#filteredEvents} should encapsulate all needed - * synchronization internally. - * - * TODO: refactor common code out of this class and CountsChartPane into - * {@link AbstractVisualization} + * must only be manipulated on the JavaFx thread. */ public class DetailViewPane extends AbstractVisualization { - private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); + private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName()); - private MultipleSelectionModel> treeSelectionModel; + private MultipleSelectionModel> treeSelectionModel; //these three could be injected from fxml but it was causing npe's private final DateAxis dateAxis = new DateAxis(); @@ -117,12 +107,10 @@ public class DetailViewPane extends AbstractVisualization aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - private final ObservableList highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - public ObservableList getAggregatedEvents() { - return aggregatedEvents; + public ObservableList getEventBundles() { + return chart.getEventBundles(); } public DetailViewPane(Pane partPane, Pane contextPane, Region spacer) { @@ -149,6 +137,7 @@ public class DetailViewPane extends AbstractVisualization change) -> { + while (change.next()) { change.getAddedSubList().forEach(node -> { node.applyHighlightEffect(true); @@ -211,6 +200,7 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); selectedNodes.stream().forEach((tn) -> { + for (EventStripeNode n : chart.getNodes((EventStripeNode t) -> t.getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); @@ -229,12 +219,13 @@ public class DetailViewPane extends AbstractVisualization> 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 (EventStripeNode n : chart.getNodes((EventStripeNode t) -> t.getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); @@ -297,11 +288,12 @@ public class DetailViewPane extends AbstractVisualization { - if (isCancelled() == false) { + + if (isCancelled() == false) { + Platform.runLater(() -> { setCursor(Cursor.WAIT); - } - }); + }); + } updateProgress(-1, 1); updateMessage(NbBundle.getMessage(this.getClass(), "DetailViewPane.loggedTask.preparing")); @@ -311,7 +303,6 @@ public class DetailViewPane extends AbstractVisualization { if (isCancelled()) { @@ -323,21 +314,24 @@ public class DetailViewPane extends AbstractVisualization eventClusters = filteredEvents.getEventClusters(); + + final int size = eventClusters.size(); + for (int i = 0; i < size; i++) { if (isCancelled()) { break; } - updateProgress(i++, size); + final EventCluster cluster = eventClusters.get(i); + updateProgress(i, size); updateMessage(NbBundle.getMessage(this.getClass(), "DetailViewPane.loggedTask.updateUI")); - final XYChart.Data xyData = new BarChart.Data<>(new DateTime(e.getSpan().getStartMillis()), e); + final XYChart.Data xyData = new BarChart.Data<>(new DateTime(cluster.getSpan().getStartMillis()), cluster); - Platform.runLater(() -> { - if (isCancelled() == false) { - getSeries(e.getEventType()).getData().add(xyData); - } - }); + if (isCancelled() == false) { + Platform.runLater(() -> { + getSeries(cluster.getEventType()).getData().add(xyData); + }); + } } Platform.runLater(() -> { @@ -345,12 +339,13 @@ public class DetailViewPane extends AbstractVisualization - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java index 2f285f103a..da07263e9f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -29,6 +29,7 @@ import java.util.Iterator; import java.util.List; 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; @@ -37,6 +38,7 @@ import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.InvalidationListener; import javafx.beans.Observable; +import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.SimpleBooleanProperty; @@ -45,6 +47,7 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Cursor; @@ -71,13 +74,15 @@ 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.datamodel.EventBundle; 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.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; -import static org.sleuthkit.autopsy.timeline.ui.detailview.Bundle.EventDetailChart_chartContextMenu_placeMarker_name; -import static org.sleuthkit.autopsy.timeline.ui.detailview.Bundle.EventDetailChart_contextMenu_zoomHistory_name; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * Custom implementation of {@link XYChart} to graph events on a horizontal @@ -95,6 +100,8 @@ import static org.sleuthkit.autopsy.timeline.ui.detailview.Bundle.EventDetailCha */ public final class EventDetailChart extends XYChart implements TimeLineChart { + 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 private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); private static final int PROJECTED_LINE_Y_OFFSET = 5; private static final int PROJECTED_LINE_STROKE_WIDTH = 5; @@ -144,16 +151,18 @@ public final class EventDetailChart extends XYChart impl * by allowing a single translation of this group. */ private final Group nodeGroup = new Group(); + private final ObservableList bundles = FXCollections.observableArrayList(); private final Map, EventStripe> stripeDescMap = new HashMap<>(); private final Map stripeNodeMap = new HashMap<>(); private final Map, Line> projectionMap = new HashMap<>(); /** - * list of series of data added to this chart TODO: replace this with a map - * from name to series? -jm + * list of series of data added to this chart + * + * TODO: replace this with a map from name to series? -jm */ - private final ObservableList> seriesList - = FXCollections.>observableArrayList(); + private final ObservableList> seriesList = + FXCollections.>observableArrayList(); private final ObservableList> sortedSeriesList = seriesList .sorted((s1, s2) -> { @@ -175,8 +184,8 @@ public final class EventDetailChart extends XYChart impl /** * how much detail of the description to show in the ui */ - private final SimpleObjectProperty descrVisibility - = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); + private final SimpleObjectProperty descrVisibility = + new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); /** * true == truncate all the labels to the greater of the size of their @@ -191,12 +200,14 @@ public final class EventDetailChart extends XYChart impl * via slider if truncateAll is true */ private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); + private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true); EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList selectedNodes) { super(dateAxis, verticalAxis); 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); @@ -248,6 +259,7 @@ public final class EventDetailChart extends XYChart impl while (c.next()) { c.getRemoved().forEach((EventStripeNode t) -> { t.getEventStripe().getRanges().forEach((Range t1) -> { + Line removedLine = projectionMap.remove(t1); getChartChildren().removeAll(removedLine); }); @@ -277,6 +289,10 @@ public final class EventDetailChart extends XYChart impl requestChartLayout(); } + ObservableList getEventBundles() { + return bundles; + } + TimeLineController getController() { return controller; } @@ -287,8 +303,9 @@ public final class EventDetailChart extends XYChart impl if (chartContextMenu != null) { chartContextMenu.hide(); } + chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(clickEvent), - new ActionGroup(EventDetailChart_contextMenu_zoomHistory_name(), + new ActionGroup(Bundle.EventDetailChart_contextMenu_zoomHistory_name(), new Back(controller), new Forward(controller)))); chartContextMenu.setAutoHide(true); @@ -309,6 +326,7 @@ public final class EventDetailChart extends XYChart impl public synchronized void setController(TimeLineController controller) { this.controller = controller; setModel(this.controller.getEventsModel()); + getController().getQuickHideFilters().addListener(layoutInvalidationListener); } @Override @@ -380,7 +398,7 @@ public final class EventDetailChart extends XYChart impl @Override protected synchronized void dataItemAdded(Series series, int i, Data data) { final EventCluster eventCluster = data.getYValue(); - + bundles.add(eventCluster); EventStripe eventStripe = stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()), new EventStripe(eventCluster), (EventStripe u, EventStripe v) -> { @@ -395,6 +413,7 @@ public final class EventDetailChart extends XYChart impl stripeNodeMap.put(eventStripe, stripeNode); nodeGroup.getChildren().add(stripeNode); data.setNode(stripeNode); + } @Override @@ -406,10 +425,11 @@ public final class EventDetailChart extends XYChart impl @Override protected synchronized void dataItemRemoved(Data data, Series series) { EventCluster eventCluster = data.getYValue(); - + bundles.removeAll(eventCluster); EventStripe removedStripe = stripeDescMap.remove(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription())); EventStripeNode removedNode = stripeNodeMap.remove(removedStripe); nodeGroup.getChildren().remove(removedNode); + data.setNode(null); } @@ -448,29 +468,29 @@ public final class EventDetailChart extends XYChart impl */ @Override protected synchronized void layoutPlotChildren() { - if (requiresLayout) { setCursor(Cursor.WAIT); - double minY = 0; maxY.set(0.0); - if (bandByType.get() == false) { - List nodes = new ArrayList<>(stripeNodeMap.values()); - nodes.sort(Comparator.comparing(EventStripeNode::getStartMillis)); - layoutNodes(nodes, minY, 0); - } else { - for (Series s : sortedSeriesList) { - List nodes = s.getData().stream() - .map(Data::getYValue) - .map(cluster -> stripeDescMap.get(ImmutablePair.of(cluster.getEventType(), cluster.getDescription()))) - .distinct() - .sorted(Comparator.comparing(EventStripe::getStartMillis)) - .map(stripeNodeMap::get) - .collect(Collectors.toList()); - layoutNodes(nodes, minY, 0); + Map> hiddenPartition; + if (bandByType.get()) { + double minY = 0; + for (Series series : sortedSeriesList) { + hiddenPartition = series.getData().stream().map(Data::getNode).map(EventStripeNode.class::cast) + .collect(Collectors.partitioningBy(node -> getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(node.getDescription())))); + + layoutNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, 0); minY = maxY.get(); } + } else { + hiddenPartition = stripeNodeMap.values().stream() + .collect(Collectors.partitioningBy(node -> getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(node.getDescription())))); + layoutNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), 0, 0); } setCursor(null); requiresLayout = false; @@ -478,6 +498,36 @@ public final class EventDetailChart extends XYChart impl layoutProjectionMap(); } + /** + * + * @param hiddenNodes the value of hiddenNodes + * @param shownNodes the value of shownNodes + * @param minY the value of minY + * @param children the value of children + * @param xOffset the value of xOffset + * + * @return the double + */ + private double layoutNodesHelper(List hiddenNodes, List shownNodes, double minY, final double xOffset) { + + hiddenNodes.forEach((EventStripeNode t) -> { +// children.remove(t); + t.setVisible(false); + t.setManaged(false); + }); + + shownNodes.forEach((EventStripeNode t) -> { +// if (false == children.contains(t)) { +// children.add(t); +// } + t.setVisible(true); + t.setManaged(true); + }); + + shownNodes.sort(Comparator.comparing(EventStripeNode::getStartMillis)); + return layoutNodes(shownNodes, minY, xOffset); + } + @Override protected synchronized void seriesAdded(Series series, int i) { for (int j = 0; j < series.getData().size(); j++) { @@ -501,17 +551,19 @@ public final class EventDetailChart extends XYChart impl } Iterable getNodes(Predicate p) { - Collection values = stripeNodeMap.values(); - //collapse tree of DetailViewNoeds to list and then filter on given predicate - return values.stream() - .flatMap(EventDetailChart::flatten) - .filter(p).collect(Collectors.toList()); - } + Function> flattener = + new Function>() { + @Override + public Stream apply(EventStripeNode node) { + return Stream.concat( + Stream.of(node), + node.getSubNodes().stream().flatMap(this::apply)); + } + }; - private static Stream flatten(EventStripeNode node) { - return Stream.concat( - Stream.of(node), - node.getSubNodes().stream().flatMap(EventDetailChart::flatten)); + return stripeNodeMap.values().stream() + .flatMap(flattener) + .filter(p).collect(Collectors.toList()); } Iterable getAllNodes() { @@ -535,31 +587,37 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - private synchronized double layoutNodes(final Collection< EventStripeNode> nodes, final double minY, final double xOffset - ) { + private synchronized double layoutNodes(final Collection< EventStripeNode> nodes, final double minY, final double xOffset) { //hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot Map maxXatY = new HashMap<>(); double localMax = minY; //for each node lay size it and position it in first available slot - for (EventStripeNode node : nodes) { - node.setDescriptionVisibility(descrVisibility.get()); - double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis())); + + for (EventStripeNode stripeNode : nodes) { + + stripeNode.setDescriptionVisibility(descrVisibility.get()); + double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis())); //position of start and end according to range of axis double startX = rawDisplayPosition - xOffset; double layoutNodesResultHeight = 0; double span = 0; - List subNodes = node.getSubNodes(); + + List subNodes = stripeNode.getSubNodes(); if (subNodes.isEmpty() == false) { - subNodes.sort(Comparator.comparing(EventStripeNode::getStartMillis)); - layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition); + Map> hiddenPartition = subNodes.stream() + .collect(Collectors.partitioningBy(testNode -> getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(testNode.getDescription())))); + + layoutNodesResultHeight = layoutNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, rawDisplayPosition); } List spanWidths = new ArrayList<>(); - double x = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis()));; + double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));; double x2; - Iterator> ranges = node.getStripe().getRanges().iterator(); + Iterator> ranges = stripeNode.getEventStripe().getRanges().iterator(); Range range = ranges.next(); do { x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); @@ -582,21 +640,21 @@ public final class EventDetailChart extends XYChart impl } while (ranges.hasNext()); - node.setSpanWidths(spanWidths); + stripeNode.setSpanWidths(spanWidths); if (truncateAll.get()) { //if truncate option is selected limit width of description label - node.setDescriptionWidth(Math.max(span, truncateWidth.get())); + stripeNode.setDescriptionWidth(Math.max(span, truncateWidth.get())); } else { //else set it unbounded - node.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); + stripeNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); } - node.autosize(); //compute size of tlNode based on constraints and event data + stripeNode.autosize(); //compute size of tlNode based on constraints and event data //get position of right edge of node ( influenced by description label) - double xRight = startX + node.getWidth(); + double xRight = startX + stripeNode.getWidth(); //get the height of the node - final double h = layoutNodesResultHeight == 0 ? node.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; + final double h = layoutNodesResultHeight == 0 ? stripeNode.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; //initial test position double yPos = minY; @@ -635,8 +693,8 @@ public final class EventDetailChart extends XYChart impl localMax = Math.max(yPos2, localMax); Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), - new KeyValue(node.layoutXProperty(), startX), - new KeyValue(node.layoutYProperty(), yPos))); + new KeyValue(stripeNode.layoutXProperty(), startX), + new KeyValue(stripeNode.layoutYProperty(), yPos))); tm.play(); } @@ -651,6 +709,7 @@ public final class EventDetailChart extends XYChart impl line.setStartX(getParentXForEpochMillis(range.lowerEndpoint())); line.setEndX(getParentXForEpochMillis(range.upperEndpoint())); + line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); } @@ -661,6 +720,17 @@ public final class EventDetailChart extends XYChart impl return getXAxis().localToParent(getXAxis().getDisplayPosition(dateTime), 0).getX(); } + /** + * @return the filteredEvents + */ + public FilteredEventsModel getFilteredEvents() { + return filteredEvents; + } + + Property alternateLayoutProperty() { + return alternateLayout; + } + static private class DetailIntervalSelector extends IntervalSelector { DetailIntervalSelector(double x, double height, Axis axis, TimeLineController controller) { @@ -686,7 +756,7 @@ public final class EventDetailChart extends XYChart impl private class PlaceMarkerAction extends Action { PlaceMarkerAction(MouseEvent clickEvent) { - super(EventDetailChart_chartContextMenu_placeMarker_name()); + super(Bundle.EventDetailChart_chartContextMenu_placeMarker_name()); setGraphic(new ImageView(MARKER)); // NON-NLS setEventHandler(actionEvent -> { @@ -707,4 +777,43 @@ public final class EventDetailChart extends XYChart impl }); } } + + class HideDescriptionAction extends Action { + + HideDescriptionAction(String description, DescriptionLoD descriptionLoD) { + super("Hide"); + 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(layoutInvalidationListener); + getController().getQuickHideFilters().add(testFilter); + return testFilter; + }); + descriptionFilter.setSelected(true); + + }); + } + } + + class UnhideDescriptionAction extends Action { + + UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) { + + super("Unhide"); + 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/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 72a6a8a4e9..666b0e01c8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -1,4 +1,5 @@ /* + * Autopsy Forensic Browser * * Copyright 2015 Basis Technology Corp. @@ -30,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.stream.Collectors; +import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleObjectProperty; import javafx.concurrent.Task; import javafx.event.ActionEvent; @@ -79,8 +81,7 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; 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.Bundle.EventStripeNode_loggedTask_name; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.SleuthkitCase; @@ -115,7 +116,7 @@ final public class EventStripeNode extends StackPane { b.setManaged(show); } - private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); private DescriptionVisibility descrVis; private Tooltip tooltip; @@ -144,12 +145,13 @@ final public class EventStripeNode extends StackPane { private final EventStripe eventStripe; private final EventStripeNode parentNode; private final FilteredEventsModel eventsModel; + private final Button hideButton; public EventStripeNode(EventDetailChart chart, EventStripe eventStripe, EventStripeNode parentEventNode) { this.eventStripe = eventStripe; this.parentNode = parentEventNode; this.chart = chart; - descLOD.set(eventStripe.getDescriptionLOD()); + descLOD.set(eventStripe.getDescriptionLoD()); sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); eventsModel = chart.getController().getEventsModel(); final Color evtColor = getEventType().getColor(); @@ -172,10 +174,16 @@ final public class EventStripeNode extends StackPane { if (eventStripe.getEventIDsWithTags().isEmpty()) { show(tagIV, false); } + + EventDetailChart.HideDescriptionAction hideClusterAction = chart.new HideDescriptionAction(getDescription(), eventStripe.getDescriptionLoD()); + hideButton = ActionUtils.createButton(hideClusterAction, ActionUtils.ActionTextBehavior.HIDE); + configureLoDButton(hideButton); configureLoDButton(plusButton); configureLoDButton(minusButton); //initialize info hbox + infoHBox.getChildren().add(4, hideButton); + infoHBox.setMinWidth(USE_PREF_SIZE); infoHBox.setPadding(new Insets(2, 5, 2, 5)); infoHBox.setAlignment(Pos.CENTER_LEFT); @@ -227,21 +235,19 @@ final public class EventStripeNode extends StackPane { }); } - /** - * - * @param showControls the value of par - */ void showDescriptionLoDControls(final boolean showControls) { DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), eventType -> new DropShadow(10, eventType.getColor())); clustersHBox.setEffect(showControls ? dropShadow : null); show(minusButton, showControls); show(plusButton, showControls); + show(hideButton, showControls); } public void setSpanWidths(List spanWidths) { for (int i = 0; i < spanWidths.size(); i++) { Region spanRegion = (Region) clustersHBox.getChildren().get(i); + Double w = spanWidths.get(i); spanRegion.setPrefWidth(w); spanRegion.setMaxWidth(w); @@ -249,10 +255,19 @@ final public class EventStripeNode extends StackPane { } } - EventStripe getStripe() { + public EventStripe getEventStripe() { return eventStripe; } + Collection makeBundlesFromClusters(List eventClusters) { + return eventClusters.stream().collect( + Collectors.toMap( + EventCluster::getDescription, //key + EventStripe::new, //value + EventStripe::merge)//merge method + ).values(); + } + @NbBundle.Messages({"# {0} - counts", "# {1} - event type", "# {2} - description", @@ -266,10 +281,10 @@ final public class EventStripeNode extends StackPane { @Override protected String call() throws Exception { HashMap hashSetCounts = new HashMap<>(); - if (!getStripe().getEventIDsWithHashHits().isEmpty()) { + if (!eventStripe.getEventIDsWithHashHits().isEmpty()) { hashSetCounts = new HashMap<>(); try { - for (TimeLineEvent tle : eventsModel.getEventsById(getStripe().getEventIDsWithHashHits())) { + for (TimeLineEvent tle : eventsModel.getEventsById(eventStripe.getEventIDsWithHashHits())) { Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); for (String hashSetName : hashSetNames) { hashSetCounts.merge(hashSetName, 1L, Long::sum); @@ -347,7 +362,7 @@ final public class EventStripeNode extends StackPane { RootFilter getSubClusterFilter() { RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf(); subClusterFilter.getSubFilters().addAll( - new DescriptionFilter(eventStripe.getDescriptionLOD(), eventStripe.getDescription()), + new DescriptionFilter(eventStripe.getDescriptionLoD(), eventStripe.getDescription(), DescriptionFilter.FilterMode.INCLUDE), new TypeFilter(getEventType())); return subClusterFilter; } @@ -383,7 +398,7 @@ final public class EventStripeNode extends StackPane { } } - private DescriptionLOD getDescriptionLoD() { + private DescriptionLoD getDescriptionLoD() { return descLOD.get(); } @@ -394,10 +409,14 @@ final public class EventStripeNode extends StackPane { * @param expand */ @NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters") - private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) { + private synchronized void loadSubBundles(DescriptionLoD.RelativeDetail relativeDetail) { + chart.getEventBundles().removeIf(bundle -> + getSubNodes().stream().anyMatch(subNode -> + bundle.equals(subNode.getEventStripe())) + ); subNodePane.getChildren().clear(); - if (descLOD.get().withRelativeDetail(relativeDetail) == eventStripe.getDescriptionLOD()) { - descLOD.set(eventStripe.getDescriptionLOD()); + if (descLOD.get().withRelativeDetail(relativeDetail) == eventStripe.getDescriptionLoD()) { + descLOD.set(eventStripe.getDescriptionLoD()); clustersHBox.setVisible(true); chart.setRequiresLayout(true); chart.requestChartLayout(); @@ -415,24 +434,25 @@ final public class EventStripeNode extends StackPane { final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); - Task> loggedTask = new Task>() { + Task> loggedTask = new Task>() { - private volatile DescriptionLOD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); + private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); { - updateTitle(EventStripeNode_loggedTask_name()); + updateTitle(Bundle.EventStripeNode_loggedTask_name()); } @Override - protected Set call() throws Exception { + protected Collection call() throws Exception { Collection bundles; - DescriptionLOD next = loadedDescriptionLoD; + DescriptionLoD next = loadedDescriptionLoD; do { loadedDescriptionLoD = next; - if (loadedDescriptionLoD == eventStripe.getDescriptionLOD()) { + if (loadedDescriptionLoD == eventStripe.getDescriptionLoD()) { return Collections.emptySet(); } bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream() + .map(cluster -> cluster.withParent(getEventStripe())) .collect(Collectors.toMap( EventCluster::getDescription, //key EventStripe::new, //value @@ -442,24 +462,28 @@ final public class EventStripeNode extends StackPane { } while (bundles.size() == 1 && nonNull(next)); // return list of AbstractEventStripeNodes representing sub-bundles - return bundles.stream() - .map(EventStripeNode.this::getNodeForBundle) - .collect(Collectors.toSet()); + return bundles; + } @Override protected void succeeded() { chart.setCursor(Cursor.WAIT); try { - Set subBundleNodes = get(); - if (subBundleNodes.isEmpty()) { + Collection bundles = get(); + + if (bundles.isEmpty()) { clustersHBox.setVisible(true); } else { clustersHBox.setVisible(false); + chart.getEventBundles().addAll(bundles); + subNodePane.getChildren().setAll(bundles.stream() + .map(EventStripeNode.this::getNodeForBundle) + .collect(Collectors.toSet())); } descLOD.set(loadedDescriptionLoD); //assign subNodes and request chart layout - subNodePane.getChildren().setAll(subBundleNodes); + chart.setRequiresLayout(true); chart.requestChartLayout(); } catch (InterruptedException | ExecutionException ex) { @@ -504,10 +528,6 @@ final public class EventStripeNode extends StackPane { } } - public EventStripe getEventStripe() { - return eventStripe; - } - Set getEventsIDs() { return eventStripe.getEventIDs(); } @@ -531,9 +551,9 @@ final public class EventStripeNode extends StackPane { } else if (t.isShortcutDown()) { chart.selectedNodes.removeAll(EventStripeNode.this); } else if (t.getClickCount() > 1) { - final DescriptionLOD next = descLOD.get().moreDetailed(); + final DescriptionLoD next = descLOD.get().moreDetailed(); if (next != null) { - loadSubBundles(DescriptionLOD.RelativeDetail.MORE); + loadSubBundles(DescriptionLoD.RelativeDetail.MORE); } } else { @@ -566,13 +586,13 @@ final public class EventStripeNode extends StackPane { setGraphic(new ImageView(PLUS)); setEventHandler((ActionEvent t) -> { - final DescriptionLOD next = descLOD.get().moreDetailed(); + final DescriptionLoD next = descLOD.get().moreDetailed(); if (next != null) { - loadSubBundles(DescriptionLOD.RelativeDetail.MORE); + loadSubBundles(DescriptionLoD.RelativeDetail.MORE); } }); - disabledProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); + disabledProperty().bind(descLOD.isEqualTo(DescriptionLoD.FULL)); } } @@ -584,12 +604,12 @@ final public class EventStripeNode extends StackPane { setGraphic(new ImageView(MINUS)); setEventHandler((ActionEvent t) -> { - final DescriptionLOD previous = descLOD.get().lessDetailed(); + final DescriptionLoD previous = descLOD.get().lessDetailed(); if (previous != null) { - loadSubBundles(DescriptionLOD.RelativeDetail.LESS); + loadSubBundles(DescriptionLoD.RelativeDetail.LESS); } }); - disabledProperty().bind(descLOD.isEqualTo(eventStripe.getDescriptionLOD())); + disabledProperty().bind(Bindings.createBooleanBinding(() -> nonNull(eventStripe) && descLOD.get() == eventStripe.getDescriptionLoD(), descLOD)); } } } 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 69fdd1fe43..b3e80eb827 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 @@ -19,8 +19,11 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; +import java.util.Deque; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javafx.collections.FXCollections; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; /** @@ -28,34 +31,60 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; */ class EventDescriptionTreeItem extends NavTreeItem { - public EventDescriptionTreeItem(EventCluster g) { - setValue(new NavTreeNode(g.getEventType().getBaseType(), g.getDescription(), g.getEventIDs().size())); + /** + * maps a description to the child item of this item with that description + */ + private final Map childMap = new ConcurrentHashMap<>(); + private final EventBundle bundle; + + public EventBundle getEventBundle() { + return bundle; + } + + EventDescriptionTreeItem(EventBundle g) { + bundle = g; + setValue(g); } @Override - public int getCount() { + public long getCount() { return getValue().getCount(); } - @Override - public void insert(EventCluster g) { - NavTreeNode value = getValue(); - if ((value.getType().getBaseType().equals(g.getEventType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) { - throw new IllegalArgumentException(); + public void insert(Deque path) { + EventBundle head = path.removeFirst(); + EventDescriptionTreeItem treeItem = childMap.get(head.getDescription()); + if (treeItem == null) { + treeItem = new EventDescriptionTreeItem(head); + treeItem.setExpanded(true); + childMap.put(head.getDescription(), treeItem); + getChildren().add(treeItem); + FXCollections.sort(getChildren(), TreeComparator.Description); } - setValue(new NavTreeNode(value.getType().getBaseType(), value.getDescription(), value.getCount() + g.getEventIDs().size())); + if (path.isEmpty() == false) { + treeItem.insert(path); + } } @Override - public void resort(Comparator> comp) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + public void resort(Comparator> comp) { + FXCollections.sort(getChildren(), comp); } @Override - public TreeItem findTreeItemForEvent(EventBundle t) { - if (getValue().getType().getBaseType() == t.getEventType().getBaseType() && getValue().getDescription().equals(t.getDescription())) { + public NavTreeItem findTreeItemForEvent(EventBundle t) { + + if (getValue().getEventType() == t.getEventType() + && getValue().getDescription().equals(t.getDescription())) { return this; + } else { + for (EventDescriptionTreeItem child : childMap.values()) { + final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); + if (findTreeItemForEvent != null) { + return findTreeItemForEvent; + } + } } 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 fb2947bc42..f1b91bf202 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 @@ -19,12 +19,11 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; +import java.util.Deque; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; class EventTypeTreeItem extends NavTreeItem { @@ -34,55 +33,39 @@ class EventTypeTreeItem extends NavTreeItem { */ private final Map childMap = new ConcurrentHashMap<>(); - private final Comparator> comparator = TreeComparator.Description; + private final Comparator> comparator = TreeComparator.Description; - EventTypeTreeItem(EventCluster g) { - setValue(new NavTreeNode(g.getEventType().getBaseType(), g.getEventType().getBaseType().getDisplayName(), 0)); + EventTypeTreeItem(EventBundle g) { + setValue(g); } @Override - public int getCount() { + public long getCount() { return getValue().getCount(); } - /** - * Recursive method to add a grouping at a given path. - * - * @param path Full path (or subset not yet added) to add - * @param g Group to add - * @param tree True if it is part of a tree (versus a list) - */ - @Override - public void insert(EventCluster g) { - - EventDescriptionTreeItem treeItem = childMap.get(g.getDescription()); + public void insert(Deque path) { + EventBundle head = path.removeFirst(); + EventDescriptionTreeItem treeItem = childMap.get(head.getDescription()); if (treeItem == null) { - final EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(g); - newTreeItem.setExpanded(true); - childMap.put(g.getDescription(), newTreeItem); - - Platform.runLater(() -> { - synchronized (getChildren()) { - getChildren().add(newTreeItem); - FXCollections.sort(getChildren(), comparator); - } - }); - } else { - treeItem.insert(g); + treeItem = new EventDescriptionTreeItem(head); + treeItem.setExpanded(true); + childMap.put(head.getDescription(), treeItem); + getChildren().add(treeItem); + FXCollections.sort(getChildren(), comparator); } - Platform.runLater(() -> { - NavTreeNode value1 = getValue(); - setValue(new NavTreeNode(value1.getType().getBaseType(), value1.getType().getBaseType().getDisplayName(), childMap.values().stream().mapToInt(EventDescriptionTreeItem::getCount).sum())); - }); + if (path.isEmpty() == false) { + treeItem.insert(path); + } } @Override - public TreeItem findTreeItemForEvent(EventBundle t) { - if (t.getEventType().getBaseType() == getValue().getType().getBaseType()) { + public NavTreeItem findTreeItemForEvent(EventBundle t) { + if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) { - for (TreeItem child : getChildren()) { - final TreeItem findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t); + for (EventDescriptionTreeItem child : childMap.values()) { + final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); if (findTreeItemForEvent != null) { return findTreeItemForEvent; } @@ -92,7 +75,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public void resort(Comparator> comp) { + public void resort(Comparator> comp) { FXCollections.sort(getChildren(), comp); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java index 57f2270f5b..9b8378ed6b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,15 +18,17 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; -import java.net.URL; +import com.google.common.collect.ImmutableList; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; -import java.util.ResourceBundle; -import javafx.application.Platform; +import java.util.Objects; +import javafx.beans.InvalidationListener; import javafx.beans.Observable; -import javafx.collections.ObservableList; +import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.scene.control.ComboBox; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.SelectionMode; import javafx.scene.control.Tooltip; @@ -36,19 +38,26 @@ import javafx.scene.control.TreeView; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; 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.ActionUtils; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineView; -import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; 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.detailview.DetailViewPane; /** - * Display two trees. one shows all folders (groups) and calls out folders with - * images. the user can select folders with images to see them in the main - * GroupListPane The other shows folders with hash set hits. + * Shows all {@link EventBundles} from the assigned {@link DetailViewPane} in a + * tree organized by type and then description. Hidden bundles are shown grayed + * out. Right clicking on a item in the tree shows a context menu to show/hide + * it. */ public class NavPanel extends BorderPane implements TimeLineView { @@ -56,59 +65,51 @@ public class NavPanel extends BorderPane implements TimeLineView { private FilteredEventsModel filteredEvents; - @FXML - private ResourceBundle resources; - - @FXML - private URL location; - private DetailViewPane detailViewPane; - /** - * TreeView for folders with hash hits - */ @FXML - private TreeView< NavTreeNode> eventsTree; + private TreeView eventsTree; @FXML private Label eventsTreeLabel; @FXML - private ComboBox>> sortByBox; + private ComboBox>> sortByBox; public NavPanel() { - - FXMLConstructor.construct(this, "NavPanel.fxml"); // NON-NLS + FXMLConstructor.construct(this, "NavPanel.fxml"); // NON-NLS } - public void setChart(DetailViewPane detailViewPane) { + public void setDetailViewPane(DetailViewPane detailViewPane) { this.detailViewPane = detailViewPane; detailViewPane.setSelectionModel(eventsTree.getSelectionModel()); - setRoot(); - detailViewPane.getAggregatedEvents().addListener((Observable observable) -> { + + detailViewPane.getEventBundles().addListener((Observable observable) -> { setRoot(); }); + setRoot(); + detailViewPane.getSelectedNodes().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); detailViewPane.getSelectedNodes().forEach(eventStripeNode -> { - eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(eventStripeNode.getEventStripe())); + eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventStripeNode.getEventStripe())); }); }); } + private NavTreeItem getRoot() { + return (NavTreeItem) eventsTree.getRoot(); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void setRoot() { RootItem root = new RootItem(); - final ObservableList aggregatedEvents = detailViewPane.getAggregatedEvents(); - - synchronized (aggregatedEvents) { - for (EventCluster agg : aggregatedEvents) { - root.insert(agg); - } + for (EventBundle bundle : detailViewPane.getEventBundles()) { + root.insert(bundle); } - Platform.runLater(() -> { - eventsTree.setRoot(root); - }); + eventsTree.setRoot(root); + } @Override @@ -130,40 +131,107 @@ public class NavPanel extends BorderPane implements TimeLineView { sortByBox.getItems().setAll(Arrays.asList(TreeComparator.Description, TreeComparator.Count)); sortByBox.getSelectionModel().select(TreeComparator.Description); sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> { - ((NavTreeItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem()); + getRoot().resort(sortByBox.getSelectionModel().getSelectedItem()); }); eventsTree.setShowRoot(false); - eventsTree.setCellFactory((TreeView p) -> new EventTreeCell()); + eventsTree.setCellFactory((TreeView p) -> new EventBundleTreeCell()); eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); eventsTreeLabel.setText(NbBundle.getMessage(this.getClass(), "NavPanel.eventsTreeLabel.text")); } /** - * A tree cell to display {@link NavTreeNode}s. Shows the description, and + * A tree cell to display {@link EventBundle}s. Shows the description, and * count, as well a a "legend icon" for the event type. */ - private static class EventTreeCell extends TreeCell { + private class EventBundleTreeCell extends TreeCell { + + private static final double HIDDEN_MULTIPLIER = .6; + private final Rectangle rect = new Rectangle(24, 24); + private final ImageView imageView = new ImageView(); + private InvalidationListener filterStateChangeListener; + + EventBundleTreeCell() { + rect.setArcHeight(5); + rect.setArcWidth(5); + rect.setStrokeWidth(2); + } @Override - protected void updateItem(NavTreeNode item, boolean empty) { + protected void updateItem(EventBundle item, boolean empty) { super.updateItem(item, empty); - if (item != null) { - final String text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS - setText(text); - setTooltip(new Tooltip(text)); - Rectangle rect = new Rectangle(24, 24); - rect.setArcHeight(5); - rect.setArcWidth(5); - rect.setStrokeWidth(2); - rect.setStroke(item.getType().getColor()); - rect.setFill(item.getType().getColor().deriveColor(0, 1, 1, 0.1)); - setGraphic(new StackPane(rect, new ImageView(item.getType().getFXImage()))); - } else { + if (item == null || 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 = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS + TreeItem parent = getTreeItem().getParent(); + if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) { + text = StringUtils.substringAfter(text, parent.getValue().getDescription()); + } + setText(text); + setTooltip(new Tooltip(text)); + imageView.setImage(item.getEventType().getFXImage()); + setGraphic(new StackPane(rect, imageView)); + updateHiddenState(item); } } + + private void registerListeners(Collection filters, EventBundle item) { + for (DescriptionFilter filter : filters) { + if (filter.getDescription().equals(item.getDescription())) { + filter.activeProperty().addListener(filterStateChangeListener); + } + } + } + + private void deRegisterListeners(Collection filters) { + if (Objects.nonNull(filterStateChangeListener)) { + for (DescriptionFilter filter : filters) { + filter.activeProperty().removeListener(filterStateChangeListener); + } + } + } + + private void updateHiddenState(EventBundle item) { + TreeItem treeItem = getTreeItem(); + ContextMenu newMenu; + if (controller.getQuickHideFilters().stream(). + filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(item.getDescription()))) { + if (treeItem != null) { + 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)); + newMenu = ActionUtils.createContextMenu(ImmutableList.of(detailViewPane.newUnhideDescriptionAction(item.getDescription(), item.getDescriptionLoD()))); + } else { + setTextFill(Color.BLACK); + imageView.setOpacity(1); + rect.setStroke(item.getEventType().getColor()); + rect.setFill(item.getEventType().getColor().deriveColor(0, 1, 1, 0.1)); + newMenu = ActionUtils.createContextMenu(ImmutableList.of(detailViewPane.newHideDescriptionAction(item.getDescription(), item.getDescriptionLoD()))); + } + if (treeItem instanceof EventDescriptionTreeItem) { + setContextMenu(newMenu); + } else { + setContextMenu(null); + } + } + } } 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 cfd582433e..d1df7a5b38 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 @@ -21,22 +21,18 @@ 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.EventCluster; /** * A node in the nav tree. Manages inserts and resorts. Has parents and * children. Does not have graphical properties these are configured in - * {@link EventTreeCell}. Each GroupTreeItem has a NavTreeNode which has a type, - * description , and count + * {@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 void insert(EventCluster g); + abstract long getCount(); - abstract int getCount(); - - abstract void resort(Comparator> comp); - - abstract TreeItem findTreeItemForEvent(EventBundle t); + abstract void resort(Comparator> comp); + abstract NavTreeItem findTreeItemForEvent(EventBundle t); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java deleted file mode 100644 index 4d193da57b..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2014 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.tree; - -import javax.annotation.concurrent.Immutable; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; - -/** - * The data item for the nav tree. Represents a combination of type and - * description, as well as the corresponding number of events - */ -@Immutable -public class NavTreeNode { - - final private EventType type; - - final private String Description; - - final private int count; - - public EventType getType() { - return type; - } - - public String getDescription() { - return Description; - } - - public int getCount() { - return count; - } - - public NavTreeNode(EventType type, String Description, int count) { - this.type = type; - this.Description = Description; - this.count = count; - } - -} 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 cb15a0b622..a96dfaae20 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 @@ -18,13 +18,13 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; +import java.util.ArrayDeque; import java.util.Comparator; +import java.util.Deque; import java.util.HashMap; import java.util.Map; -import javafx.application.Platform; -import javafx.collections.FXCollections; +import java.util.Optional; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -47,7 +47,7 @@ class RootItem extends NavTreeItem { } @Override - public int getCount() { + public long getCount() { return getValue().getCount(); } @@ -56,39 +56,41 @@ class RootItem extends NavTreeItem { * * @param g Group to add */ - @Override - public void insert(EventCluster g) { + public void insert(EventBundle g) { - EventTypeTreeItem treeItem = childMap.get(g.getEventType().getBaseType()); - if (treeItem == null) { - final EventTypeTreeItem newTreeItem = new EventTypeTreeItem(g); - newTreeItem.setExpanded(true); - childMap.put(g.getEventType().getBaseType(), newTreeItem); - newTreeItem.insert(g); - - Platform.runLater(() -> { - synchronized (getChildren()) { + EventTypeTreeItem treeItem = childMap.computeIfAbsent(g.getEventType().getBaseType(), + baseType -> { + EventTypeTreeItem newTreeItem = new EventTypeTreeItem(g); + newTreeItem.setExpanded(true); getChildren().add(newTreeItem); + getChildren().sort(TreeComparator.Type); + return newTreeItem; + }); + treeItem.insert(getTreePath(g)); + } - FXCollections.sort(getChildren(), TreeComparator.Type); - } - }); - } else { - treeItem.insert(g); + static Deque getTreePath(EventBundle g) { + Deque path = new ArrayDeque<>(); + Optional p = Optional.of(g); + + while (p.isPresent()) { + EventBundle parent = p.get(); + path.addFirst(parent); + p = parent.getParentBundle(); } + + return path; } @Override - public void resort(Comparator> comp) { - childMap.values().forEach((ti) -> { - ti.resort(comp); - }); + public void resort(Comparator> comp) { + childMap.values().forEach(ti -> ti.resort(comp)); } @Override - public TreeItem findTreeItemForEvent(EventBundle t) { - for (TreeItem child : getChildren()) { - final TreeItem findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t); + public NavTreeItem findTreeItemForEvent(EventBundle t) { + for (EventTypeTreeItem child : childMap.values()) { + final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); if (findTreeItemForEvent != null) { return findTreeItemForEvent; } 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 2ec4b0fa1a..aad194f61c 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 @@ -20,27 +20,27 @@ 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.eventtype.EventType; -enum TreeComparator implements Comparator> { +enum TreeComparator implements Comparator> { Description { @Override - public int compare(TreeItem o1, TreeItem o2) { + public int compare(TreeItem o1, TreeItem o2) { return o1.getValue().getDescription().compareTo(o2.getValue().getDescription()); } }, Count { @Override - public int compare(TreeItem o1, TreeItem o2) { - - return -Integer.compare(o1.getValue().getCount(), o2.getValue().getCount()); + public int compare(TreeItem o1, TreeItem o2) { + return Long.compare(o2.getValue().getCount(), o1.getValue().getCount()); } }, Type { @Override - public int compare(TreeItem o1, TreeItem o2) { - return EventType.getComparator().compare(o1.getValue().getType(), o2.getValue().getType()); + public int compare(TreeItem o1, TreeItem o2) { + return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType()); } }; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java deleted file mode 100644 index d53aadce15..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2015 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.filtering; - -import javafx.application.Platform; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.scene.control.CheckBox; -import javafx.scene.control.TreeTableCell; -import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; - -/** - * A {@link TreeTableCell} that represents the active state of a - * {@link AbstractFilter} as a checkbox - */ -class FilterCheckBoxCell extends TreeTableCell { - - private final CheckBox checkBox = new CheckBox(); - private SimpleBooleanProperty activeProperty; - - @Override - protected void updateItem(AbstractFilter item, boolean empty) { - super.updateItem(item, empty); - Platform.runLater(() -> { - if (activeProperty != null) { - checkBox.selectedProperty().unbindBidirectional(activeProperty); - } - checkBox.disableProperty().unbind(); - if (item == null) { - setText(null); - setGraphic(null); - } else { - setText(item.getDisplayName()); - activeProperty = item.getSelectedProperty(); - checkBox.selectedProperty().bindBidirectional(activeProperty); - checkBox.disableProperty().bind(item.getDisabledProperty()); - setGraphic(checkBox); - } - }); - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCellFactory.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCellFactory.java new file mode 100644 index 0000000000..b367ade07c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCellFactory.java @@ -0,0 +1,42 @@ +/* + * 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.function.Supplier; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.scene.control.CheckBox; +import javafx.scene.control.IndexedCell; +import org.sleuthkit.autopsy.timeline.ui.AbstractFXCellFactory; +import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; + +class FilterCheckBoxCellFactory extends AbstractFXCellFactory { + + private final CheckBox checkBox = new CheckBox(); + private SimpleBooleanProperty selectedProperty; + private SimpleBooleanProperty disabledProperty; + + @Override + protected void configureCell(IndexedCell cell, X item, boolean empty, Supplier supplier) { + if (selectedProperty != null) { + checkBox.selectedProperty().unbindBidirectional(selectedProperty); + } + if (disabledProperty != null) { + checkBox.disableProperty().unbindBidirectional(disabledProperty); + } + + if (item == null) { + cell.setText(null); + cell.setGraphic(null); + } else { + cell.setText(item.getDisplayName()); + selectedProperty = item.selectedProperty(); + checkBox.selectedProperty().bindBidirectional(selectedProperty); + disabledProperty = item.getDisabledProperty(); + checkBox.disableProperty().bindBidirectional(disabledProperty); + cell.setGraphic(checkBox); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml index 265c986360..6f6d17b81a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml @@ -1,5 +1,6 @@ + @@ -26,13 +27,23 @@ -
- - - - - - - - +
+ + + + + + + + + + + + + + + + + +
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 3f9bc03fab..506554fbb7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java @@ -22,25 +22,36 @@ import javafx.application.Platform; import javafx.beans.Observable; 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.CheckBox; import javafx.scene.control.ContextMenu; +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; import javafx.scene.layout.BorderPane; import org.controlsfx.control.action.Action; +import org.controlsfx.control.action.ActionUtils; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.VisualizationMode; import org.sleuthkit.autopsy.timeline.actions.ResetFilters; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType; import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.Filter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; @@ -54,7 +65,7 @@ import static org.sleuthkit.autopsy.timeline.ui.filtering.Bundle.Timeline_ui_fil * This also implements {@link TimeLineView} since it dynamically updates its * filters based on the contents of a {@link FilteredEventsModel} */ -public class FilterSetPanel extends BorderPane implements TimeLineView { +final public class FilterSetPanel extends BorderPane implements TimeLineView { @FXML private Button applyButton; @@ -71,14 +82,22 @@ public class FilterSetPanel extends BorderPane implements TimeLineView { @FXML private TreeTableColumn legendColumn; + @FXML + private ListView hiddenDescriptionsListView; + @FXML + private TitledPane hiddenDescriptionsPane; + @FXML + private SplitPane splitPane; + private FilteredEventsModel filteredEvents; private TimeLineController controller; private final ObservableMap expansionMap = FXCollections.observableHashMap(); + private double position; @FXML - @NbBundle.Messages({"FilterSetPanel.applyButton.text=Apply", + @NbBundle.Messages({ "Timeline.ui.filtering.menuItem.all=all", "FilterSetPanel.defaultButton.text=Default", "Timeline.ui.filtering.menuItem.none=none", @@ -88,10 +107,7 @@ public class FilterSetPanel extends BorderPane implements TimeLineView { void initialize() { assert applyButton != null : "fx:id=\"applyButton\" was not injected: check your FXML file 'FilterSetPanel.fxml'."; // NON-NLS - applyButton.setOnAction(e -> { - controller.pushFilters((RootFilter) filterTreeTable.getRoot().getValue().copyOf()); - }); - applyButton.setText(Bundle.FilterSetPanel_applyButton_text()); + ActionUtils.configureButton(new ApplyFiltersAction(), applyButton); defaultButton.setText(Bundle.FilterSetPanel_defaultButton_text()); //remove column headers via css. @@ -148,7 +164,7 @@ public class FilterSetPanel extends BorderPane implements TimeLineView { //configure tree column to show name of filter and checkbox treeColumn.setCellValueFactory(param -> param.getValue().valueProperty()); - treeColumn.setCellFactory(col -> new FilterCheckBoxCell()); + 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()); @@ -158,7 +174,23 @@ public class FilterSetPanel extends BorderPane implements TimeLineView { public FilterSetPanel() { FXMLConstructor.construct(this, "FilterSetPanel.fxml"); // NON-NLS - expansionMap.put(new TypeFilter(RootEventType.getInstance()).getDisplayName(), Boolean.TRUE); + expansionMap.put(new TypeFilter(RootEventType.getInstance()).getDisplayName(), true); + } + + static class ListCellImpl extends ListCell { + + @Override + protected void updateItem(DescriptionFilter item, boolean empty) { + super.updateItem(item, empty); //To change body of generated methods, choose Tools | Templates. + if (item == null || empty) { + setText(null); + setGraphic(null); + } else { + setGraphic(new CheckBox()); + setText(item.getDisplayName()); + } + } + } @Override @@ -168,20 +200,106 @@ public class FilterSetPanel extends BorderPane implements TimeLineView { defaultButton.setOnAction(defaultFiltersAction); defaultButton.disableProperty().bind(defaultFiltersAction.disabledProperty()); this.setModel(timeLineController.getEventsModel()); + + 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("Unhide and remove from list"); + } else { + setText("Remove from list"); + } + } + })); + } + }); + + return forList; + }); + +// hiddenDescriptionsPane.setContent(null); + controller.viewModeProperty().addListener(observable -> { + applyFilters(); + if (controller.viewModeProperty().get() == VisualizationMode.COUNTS) { + position = splitPane.getDividerPositions()[0]; + splitPane.setDividerPositions(1); + hiddenDescriptionsPane.setExpanded(false); + hiddenDescriptionsPane.setCollapsible(false); + hiddenDescriptionsPane.setDisable(true); + } else { + splitPane.setDividerPositions(position); + hiddenDescriptionsPane.setDisable(false); + hiddenDescriptionsPane.setCollapsible(true); + hiddenDescriptionsPane.setExpanded(true); + hiddenDescriptionsPane.setCollapsible(false); + + } + }); } @Override + public void setModel(FilteredEventsModel filteredEvents) { this.filteredEvents = filteredEvents; - refresh(); + this.filteredEvents.eventTypeZoomProperty().addListener((Observable observable) -> { + applyFilters(); + }); + this.filteredEvents.descriptionLODProperty().addListener((Observable observable) -> { + applyFilters(); + }); + this.filteredEvents.timeRangeProperty().addListener((Observable observable) -> { + applyFilters(); + }); this.filteredEvents.filterProperty().addListener((Observable o) -> { refresh(); }); + refresh(); + } private void refresh() { Platform.runLater(() -> { - filterTreeTable.setRoot(new FilterTreeItem(filteredEvents.filterProperty().get().copyOf(), expansionMap)); + filterTreeTable.setRoot(new FilterTreeItem(filteredEvents.getFilter().copyOf(), expansionMap)); }); } + + @NbBundle.Messages({"FilterSetPanel.applyButton.text=Apply"}) + private class ApplyFiltersAction extends Action { + + ApplyFiltersAction() { + super(Bundle.FilterSetPanel_applyButton_text()); + setLongText("(Re)Apply filters"); + setGraphic(new ImageView(TICK)); + setEventHandler((ActionEvent t) -> { + applyFilters(); + }); + } + } + + private void applyFilters() { + Platform.runLater(() -> { + controller.pushFilters((RootFilter) filterTreeTable.getRoot().getValue()); + }); + + } + + private static final Image TICK = new Image("org/sleuthkit/autopsy/timeline/images/tick.png"); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java index d1b71e85e4..adc57dc646 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java @@ -11,7 +11,7 @@ import org.sleuthkit.autopsy.timeline.filters.Filter; /** * A TreeItem for a filter. */ -public class FilterTreeItem extends TreeItem { +final public class FilterTreeItem extends TreeItem { /** * recursively construct a tree of treeitems to parallel the filter tree of @@ -40,16 +40,17 @@ public class FilterTreeItem extends TreeItem { }); if (f instanceof CompoundFilter) { - CompoundFilter cf = (CompoundFilter) f; + CompoundFilter compoundFilter = (CompoundFilter) f; - for (Filter af : cf.getSubFilters()) { - getChildren().add(new FilterTreeItem(af, expansionMap)); + for (Filter subFilter : compoundFilter.getSubFilters()) { + getChildren().add(new FilterTreeItem(subFilter, expansionMap)); } - cf.getSubFilters().addListener((ListChangeListener.Change c) -> { + compoundFilter.getSubFilters().addListener((ListChangeListener.Change c) -> { while (c.next()) { - for (Filter af : c.getAddedSubList()) { - getChildren().add(new FilterTreeItem(af, expansionMap)); + for (Filter subfFilter : c.getAddedSubList()) { + setExpanded(true); + getChildren().add(new FilterTreeItem(subfFilter, expansionMap)); } } }); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java similarity index 70% rename from Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java rename to Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java index 8990d2ce21..50d612b210 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java @@ -23,11 +23,11 @@ import org.openide.util.NbBundle; /** * Enumeration of all description levels of detail. */ -public enum DescriptionLOD { +public enum DescriptionLoD { - SHORT(NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.short")), - MEDIUM(NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.medium")), - FULL(NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.full")); + SHORT(NbBundle.getMessage(DescriptionLoD.class, "DescriptionLOD.short")), + MEDIUM(NbBundle.getMessage(DescriptionLoD.class, "DescriptionLOD.medium")), + FULL(NbBundle.getMessage(DescriptionLoD.class, "DescriptionLOD.full")); private final String displayName; @@ -35,11 +35,11 @@ public enum DescriptionLOD { return displayName; } - private DescriptionLOD(String displayName) { + private DescriptionLoD(String displayName) { this.displayName = displayName; } - public DescriptionLOD moreDetailed() { + public DescriptionLoD moreDetailed() { try { return values()[ordinal() + 1]; } catch (ArrayIndexOutOfBoundsException e) { @@ -47,7 +47,7 @@ public enum DescriptionLOD { } } - public DescriptionLOD lessDetailed() { + public DescriptionLoD lessDetailed() { try { return values()[ordinal() - 1]; } catch (ArrayIndexOutOfBoundsException e) { @@ -55,7 +55,7 @@ public enum DescriptionLOD { } } - public DescriptionLOD withRelativeDetail(RelativeDetail relativeDetail) { + public DescriptionLoD withRelativeDetail(RelativeDetail relativeDetail) { switch (relativeDetail) { case EQUAL: return this; @@ -68,6 +68,17 @@ public enum DescriptionLOD { } } + public RelativeDetail getDetailLevelRelativeTo(DescriptionLoD other) { + int compareTo = this.compareTo(other); + if (compareTo < 0) { + return RelativeDetail.LESS; + } else if (compareTo == 0) { + return RelativeDetail.EQUAL; + } else { + return RelativeDetail.MORE; + } + } + public enum RelativeDetail { EQUAL, diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java index 2bb4524e66..2bb289f091 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java @@ -21,7 +21,6 @@ package org.sleuthkit.autopsy.timeline.zooming; import java.util.Objects; import org.joda.time.Interval; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.timeline.filters.Filter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; /** @@ -36,7 +35,7 @@ public class ZoomParams { private final RootFilter filter; - private final DescriptionLOD descrLOD; + private final DescriptionLoD descrLOD; public Interval getTimeRange() { return timeRange; @@ -50,16 +49,15 @@ public class ZoomParams { return filter; } - public DescriptionLOD getDescriptionLOD() { + public DescriptionLoD getDescriptionLOD() { return descrLOD; } - public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, RootFilter filter, DescriptionLOD descrLOD) { + public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, RootFilter filter, DescriptionLoD descrLOD) { this.timeRange = timeRange; this.typeZoomLevel = zoomLevel; this.filter = filter; this.descrLOD = descrLOD; - } public ZoomParams withTimeAndType(Interval timeRange, EventTypeZoomLevel zoomLevel) { @@ -74,7 +72,7 @@ public class ZoomParams { return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD); } - public ZoomParams withDescrLOD(DescriptionLOD descrLOD) { + public ZoomParams withDescrLOD(DescriptionLoD descrLOD) { return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD); } @@ -82,7 +80,7 @@ public class ZoomParams { return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD); } - public boolean hasFilter(Filter filterSet) { + public boolean hasFilter(RootFilter filterSet) { return this.filter.equals(filterSet); } @@ -94,7 +92,7 @@ public class ZoomParams { return this.timeRange == null ? false : this.timeRange.equals(timeRange); } - public boolean hasDescrLOD(DescriptionLOD newLOD) { + public boolean hasDescrLOD(DescriptionLoD newLOD) { return this.descrLOD.equals(newLOD); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java index 1885c90d21..b1c5719625 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java @@ -97,7 +97,7 @@ public class ZoomSettingsPane extends TitledPane implements TimeLineView { typeZoomSlider.setMin(1); typeZoomSlider.setMax(2); typeZoomSlider.setLabelFormatter(new TypeZoomConverter()); - descrLODSlider.setMax(DescriptionLOD.values().length - 1); + descrLODSlider.setMax(DescriptionLoD.values().length - 1); descrLODSlider.setLabelFormatter(new DescrLODConverter()); descrLODLabel.setText( NbBundle.getMessage(this.getClass(), "ZoomSettingsPane.descrLODLabel.text")); @@ -115,7 +115,7 @@ public class ZoomSettingsPane extends TitledPane implements TimeLineView { synchronized public void setController(TimeLineController controller) { this.controller = controller; setModel(controller.getEventsModel()); - descrLODSlider.disableProperty().bind(controller.getViewMode().isEqualTo(VisualizationMode.COUNTS)); + descrLODSlider.disableProperty().bind(controller.viewModeProperty().isEqualTo(VisualizationMode.COUNTS)); Back back = new Back(controller); backButton.disableProperty().bind(back.disabledProperty()); backButton.setOnAction(back); @@ -154,7 +154,7 @@ public class ZoomSettingsPane extends TitledPane implements TimeLineView { initializeSlider(descrLODSlider, () -> { - DescriptionLOD newLOD = DescriptionLOD.values()[Math.round(descrLODSlider.valueProperty().floatValue())]; + DescriptionLoD newLOD = DescriptionLoD.values()[Math.round(descrLODSlider.valueProperty().floatValue())]; if (controller.pushDescrLOD(newLOD) == false) { descrLODSlider.setValue(new DescrLODConverter().fromString(filteredEvents.getDescriptionLOD().toString())); } @@ -244,12 +244,12 @@ public class ZoomSettingsPane extends TitledPane implements TimeLineView { @Override public String toString(Double object) { - return DescriptionLOD.values()[object.intValue()].getDisplayName(); + return DescriptionLoD.values()[object.intValue()].getDisplayName(); } @Override public Double fromString(String string) { - return new Integer(DescriptionLOD.valueOf(string).ordinal()).doubleValue(); + return new Integer(DescriptionLoD.valueOf(string).ordinal()).doubleValue(); } } } diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml index eddc1b4f65..0f07ff2b83 100644 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -31,7 +31,6 @@ com.apple.eio com.google.common.annotations com.google.common.base - com.google.common.base.internal com.google.common.cache com.google.common.collect com.google.common.escape