diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java index 6c9e1b00ed..14394546ba 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java @@ -68,10 +68,12 @@ public abstract class AbstractFilter implements Filter { return "[" + (isSelected() ? "x" : " ") + "]"; // NON-NLS } + @Override public boolean isActive() { - return activeProperty.get(); + return activeProperty().get(); } + @Override public 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 055c1c8748..285e2186d6 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java @@ -20,7 +20,6 @@ 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; import javafx.collections.ObservableList; @@ -31,12 +30,8 @@ import javafx.collections.ObservableList; * implementations can decide how to combine the sub-filters. * * a {@link CompoundFilter} uses listeners to enforce the following - * relationships between it and its sub-filters: - *
    - * if a filter becomes inactive disable all of its subfilters - * if a sub-filter changes active state set the parent filter active if any - * of its sub-filters are active. - *
+ * relationships between it and its sub-filters: if all of a compound filter's + * sub-filters become un-selected, un-select the compound filter. */ public abstract class CompoundFilter extends AbstractFilter { @@ -57,30 +52,21 @@ public abstract class CompoundFilter extends Abstr public CompoundFilter(List subFilters) { super(); - //listen to changes in list of subfilters and add active state listener to newly added filters - this.subFilters.addListener((ListChangeListener.Change c) -> { - while (c.next()) { - addSubFilterListeners(c.getAddedSubList()); + //listen to changes in list of subfilters + this.subFilters.addListener((ListChangeListener.Change change) -> { + while (change.next()) { + //add a listener to the selected property of each added subfilter + change.getAddedSubList().forEach(addedSubFilter -> { + //if a subfilter's selected property changes... + addedSubFilter.selectedProperty().addListener(selectedProperty -> { + //set this compound filter selected af any of the subfilters are selected. + setSelected(getSubFilters().parallelStream().anyMatch(Filter::isSelected)); + }); + }); } }); - this.subFilters.setAll(subFilters); - - this.selectedProperty().addListener(activeProperty -> { - getSubFilters().forEach(subFilter -> subFilter.setDisabled(isActive() == false)); - }); - this.disabledProperty().addListener(activeProperty -> { - getSubFilters().forEach(subFilter -> subFilter.setDisabled(isActive() == false)); - }); - } - private void addSubFilterListeners(List newSubfilters) { - for (SubFilterType sf : newSubfilters) { - //if a subfilter changes active state - sf.selectedProperty().addListener((Observable observable) -> { - //set this filter acttive af any of the subfilters are active. - setSelected(getSubFilters().parallelStream().anyMatch(Filter::isSelected)); - }); - } + this.subFilters.setAll(subFilters); } static boolean areSubFiltersEqual(final CompoundFilter oneFilter, final CompoundFilter otherFilter) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java index 2cd3f93d9e..c713965933 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java @@ -43,7 +43,7 @@ public class DataSourceFilter extends AbstractFilter { @Override synchronized public DataSourceFilter copyOf() { - DataSourceFilter filterCopy = new DataSourceFilter(getDisplayName(), getDataSourceID()); + DataSourceFilter filterCopy = new DataSourceFilter(getDataSourceName(), getDataSourceID()); filterCopy.setSelected(isSelected()); filterCopy.setDisabled(isDisabled()); return filterCopy; @@ -51,7 +51,7 @@ public class DataSourceFilter extends AbstractFilter { @Override public String getDisplayName() { - return dataSourceName; + return getDataSourceName(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourcesFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourcesFilter.java index baeb3b7e61..3395982ad7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourcesFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourcesFilter.java @@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.timeline.filters; import java.util.function.Predicate; import java.util.stream.Collectors; import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; import javafx.beans.value.ObservableBooleanValue; import org.openide.util.NbBundle; @@ -29,19 +30,23 @@ import org.openide.util.NbBundle; */ public class DataSourcesFilter extends UnionFilter { + //keep references to the overridden properties so they don't get GC'd + private final BooleanBinding activePropertyOverride; + private final BooleanBinding disabledPropertyOverride; + public DataSourcesFilter() { - setSelected(false); + disabledPropertyOverride = Bindings.or(super.disabledProperty(), Bindings.size(getSubFilters()).lessThanOrEqualTo(1)); + activePropertyOverride = super.activeProperty().and(Bindings.not(disabledPropertyOverride)); } @Override public DataSourcesFilter copyOf() { final DataSourcesFilter filterCopy = new DataSourcesFilter(); + //add a copy of each subfilter + getSubFilters().forEach(dataSourceFilter -> filterCopy.addSubFilter(dataSourceFilter.copyOf())); + //these need to happen after the listeners fired by adding the subfilters filterCopy.setSelected(isSelected()); filterCopy.setDisabled(isDisabled()); - //add a copy of each subfilter - getSubFilters().forEach(dataSourceFilter -> - filterCopy.addSubFilter(dataSourceFilter.copyOf()) - ); return filterCopy; } @@ -65,13 +70,6 @@ public class DataSourcesFilter extends UnionFilter { return string; } - public void addSubFilter(DataSourceFilter dataSourceFilter) { - super.addSubFilter(dataSourceFilter); - if (getSubFilters().size() > 1) { - setSelected(Boolean.TRUE); - } - } - @Override public boolean equals(Object obj) { if (obj == null) { @@ -97,7 +95,12 @@ public class DataSourcesFilter extends UnionFilter { @Override public ObservableBooleanValue disabledProperty() { - return Bindings.or(super.disabledProperty(), Bindings.size(getSubFilters()).lessThanOrEqualTo(1)); + return disabledPropertyOverride; + } + + @Override + public BooleanBinding activeProperty() { + return activePropertyOverride; } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java index 435998a183..92e04dcf81 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java @@ -25,11 +25,16 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; /** - * Interface for Filters + * Interface for Filters. Filters are given to the EventDB who interpretes them + * a appropriately for all db queries. Since the filters are primarily + * configured in the UI, this interface provides selected, disabled and active + * (selected and not disabled) properties. */ public interface Filter { /** + * get a filter that is the intersection of the given filters + * * @param filters a set of filters to intersect * * @return a filter that is the intersection of the given filters @@ -39,50 +44,101 @@ public interface Filter { } /** + * get a filter that is the intersection of the given filters + * * @param filters a set of filters to intersect * * @return a filter that is the intersection of the given filters */ public static IntersectionFilter intersect(Filter[] filters) { - return new IntersectionFilter<>(FXCollections.observableArrayList(filters)); + return intersect(FXCollections.observableArrayList(filters)); } /** - * since filters have mutable state (active) and are observed in various - * places, we need a mechanism to copy the current state to keep in history. + * since filters have mutable state (selected/disabled/active) and are + * observed in various places, we need a mechanism to copy the current state + * to keep in the history. * - * Concrete subtasks should implement this in a way that preserves the - * active state and any subfilters. + * Concrete sub classes should implement this in a way that preserves the + * state and any sub-filters. * * @return a copy of this filter. */ Filter copyOf(); + /** + * get the display name of this filter + * + * @return a name for this filter to show in the UI + */ String getDisplayName(); + /** + * get a representation of this filter (and it's state) as a HTML string + * + * @return a html representation of this filter + */ String getHTMLReportString(); + /** + * get an Ascii representation of this filter's selected state: ie [x] for + * selected or [ ] for not selected + * + * @return an Ascii representation of this filter's selected state + */ String getStringCheckBox(); + /** + * is this filter selected + * + * @return true if this filter is selected + */ boolean isSelected(); - void setSelected(Boolean act); + /** + * set this filter selected + * + * @param selected true to selecte, false to un-select + */ + void setSelected(Boolean selected); + /** + * observable selected property + * + * @return the observable selected property for this filter + */ SimpleBooleanProperty selectedProperty(); - /* - * TODO: disabled state only affects the state of the checkboxes in the ui - * and not the actual filters and shouldn't be implemented here, but it was - * too hard to figure out how it should be implemented without intruding on - * the ui-ignorant filters + /** + * set the filter disabled */ void setDisabled(Boolean act); + /** + * observable disabled property + * + * @return the observable disabled property for this filter + */ ObservableBooleanValue disabledProperty(); + /** + * is this filter disabled + * + * @return true if this filter is disabled + */ boolean isDisabled(); + /** + * is this filter active (selected and not disabled) + * + * @return true if this filter is active + */ boolean isActive(); + /** + * observable active property + * + * @return the observable active property for this filter + */ BooleanBinding activeProperty(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/HashHitsFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/HashHitsFilter.java index c2eb196b79..ba2791ac1e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/HashHitsFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/HashHitsFilter.java @@ -42,12 +42,12 @@ public class HashHitsFilter extends UnionFilter { @Override public HashHitsFilter copyOf() { HashHitsFilter filterCopy = new HashHitsFilter(); + //add a copy of each subfilter + this.getSubFilters().forEach(hashSetFilter -> filterCopy.addSubFilter(hashSetFilter.copyOf())); + //these need to happen after the listeners fired by adding the subfilters filterCopy.setSelected(isSelected()); filterCopy.setDisabled(isDisabled()); - //add a copy of each subfilter - this.getSubFilters().forEach(hashSetFilter -> - filterCopy.addSubFilter(hashSetFilter.copyOf()) - ); + return filterCopy; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java index 2b31978d7b..8b26ed1c71 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -122,4 +122,8 @@ public class RootFilter extends IntersectionFilter { } }; } + + public TypeFilter getTypeFilter() { + return typeFilter; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/TagsFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/TagsFilter.java index 973b82a19d..7d23c49916 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/TagsFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/TagsFilter.java @@ -44,12 +44,12 @@ public class TagsFilter extends UnionFilter { @Override public TagsFilter copyOf() { TagsFilter filterCopy = new TagsFilter(); + //add a copy of each subfilter + getSubFilters().forEach(tagNameFilter -> filterCopy.addSubFilter(tagNameFilter.copyOf())); + //these need to happen after the listeners fired by adding the subfilters filterCopy.setSelected(isSelected()); filterCopy.setDisabled(isDisabled()); - //add a copy of each subfilter - getSubFilters().forEach(tagNameFilter -> - filterCopy.addSubFilter(tagNameFilter.copyOf()) - ); + return filterCopy; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java index d3dc05985e..b897791985 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java @@ -101,13 +101,11 @@ public class TypeFilter extends UnionFilter { public TypeFilter copyOf() { //make a nonrecursive copy of this filter final TypeFilter filterCopy = new TypeFilter(eventType, false); + //add a copy of each subfilter + getSubFilters().forEach(typeFilter -> filterCopy.addSubFilter(typeFilter.copyOf(), comparator)); + //these need to happen after the listeners fired by adding the subfilters filterCopy.setSelected(isSelected()); filterCopy.setDisabled(isDisabled()); - //add a copy of each subfilter - getSubFilters().forEach(typeFilter -> - filterCopy.addSubFilter(typeFilter.copyOf(), comparator) - ); - return filterCopy; } 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 760f0a9bc3..8512c3f819 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2013-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,12 +44,10 @@ import org.sleuthkit.autopsy.timeline.TimeLineController; 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; /** * The FXML controller for the filter ui. @@ -86,7 +84,11 @@ final public class FilterSetPanel extends BorderPane { private final FilteredEventsModel filteredEvents; private final TimeLineController controller; - private final ObservableMap expansionMap = FXCollections.observableHashMap(); + /** + * map from filter to its expansion state in the ui, used to restore the + * expansion state as we navigate back and forward in the history + */ + private final ObservableMap expansionMap = FXCollections.observableHashMap(); private double dividerPosition; @NbBundle.Messages({ @@ -114,7 +116,8 @@ final public class FilterSetPanel extends BorderPane { legendColumn.setCellValueFactory(cellDataFeatures -> cellDataFeatures.getValue().valueProperty()); legendColumn.setCellFactory(col -> new LegendCell(this.controller)); - expansionMap.put(new TypeFilter(RootEventType.getInstance()).getDisplayName(), true); + //type is the only filter expanded initialy + expansionMap.put(controller.getEventsModel().getFilter().getTypeFilter(), true); this.filteredEvents.eventTypeZoomProperty().addListener((Observable observable) -> applyFilters()); this.filteredEvents.descriptionLODProperty().addListener((Observable observable1) -> applyFilters()); 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 adc57dc646..58be2a2554 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java @@ -1,6 +1,23 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014-16 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.beans.Observable; import javafx.collections.ListChangeListener; import javafx.collections.MapChangeListener; import javafx.collections.ObservableMap; @@ -14,46 +31,68 @@ import org.sleuthkit.autopsy.timeline.filters.Filter; final public class FilterTreeItem extends TreeItem { /** - * recursively construct a tree of treeitems to parallel the filter tree of + * recursively construct a tree of TreeItems to parallel the filter tree of * the given filter * * - * @param f the filter for this item. if f has sub-filters, tree items will - * be made for them added added to the children of this - * FilterTreeItem + * @param filter the filter for this item. if f has sub-filters, tree items + * will be made for them added added to the children of this + * FilterTreeItem */ - public FilterTreeItem(Filter f, ObservableMap expansionMap) { - super(f); + public FilterTreeItem(Filter filter, ObservableMap expansionMap) { + super(filter); - expansionMap.addListener((MapChangeListener.Change change) -> { - if (change.getKey() == f.getDisplayName()) { + //listen to changes in the expansion map, and update expansion state of filter object + expansionMap.addListener((MapChangeListener.Change change) -> { + if (change.getKey().equals(filter)) { setExpanded(expansionMap.get(change.getKey())); } }); - if (expansionMap.get(f.getDisplayName()) != null) { - setExpanded(expansionMap.get(f.getDisplayName())); + if (expansionMap.containsKey(filter)) { + setExpanded(expansionMap.get(filter)); } - expandedProperty().addListener((Observable observable) -> { - expansionMap.put(f.getDisplayName(), isExpanded()); - }); + //keep expanion map upto date if user expands/collapses filter + expandedProperty().addListener(expandedProperty -> expansionMap.put(filter, isExpanded())); - if (f instanceof CompoundFilter) { - CompoundFilter compoundFilter = (CompoundFilter) f; + //if the filter is a compound filter, add its subfilters to the tree + if (filter instanceof CompoundFilter) { + final CompoundFilter compoundFilter = (CompoundFilter) filter; - for (Filter subFilter : compoundFilter.getSubFilters()) { - getChildren().add(new FilterTreeItem(subFilter, expansionMap)); - } + //add all sub filters + compoundFilter.getSubFilters().forEach(subFilter -> getChildren().add(new FilterTreeItem(subFilter, expansionMap))); + //listen to changes in sub filters and keep tree in sync compoundFilter.getSubFilters().addListener((ListChangeListener.Change c) -> { while (c.next()) { for (Filter subfFilter : c.getAddedSubList()) { - setExpanded(true); + setExpanded(true); //emphasize new filters by expanding parent to make sure they are visible getChildren().add(new FilterTreeItem(subfFilter, expansionMap)); } } }); + + /* + * enforce the following relationship between a compound filter and + * its subfilters: if a compound filter's active property changes, + * disable the subfilters if the compound filter is not active. + */ + compoundFilter.activeProperty().addListener(activeProperty -> { + disableSubFiltersIfNotActive(compoundFilter); + }); + + disableSubFiltersIfNotActive(compoundFilter); } } + + /** + * disable the sub-filters of the given compound filter if it is not active + * + * @param compoundFilter the compound filter + */ + static private void disableSubFiltersIfNotActive(CompoundFilter compoundFilter) { + boolean inactive = compoundFilter.isActive() == false; + compoundFilter.getSubFilters().forEach(subFilter -> subFilter.setDisabled(inactive)); + } }