/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.ui.filtering;
import java.util.Arrays;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Cell;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TitledPane;
import javafx.scene.control.TreeTableColumn;
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.actions.ResetFilters;
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.filters.Filter;
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
/**
* The FXML controller for the filter ui.
*
* This also implements TimeLineView since it dynamically updates its
* filters based on the contents of a FilteredEventsModel
*/
final public class FilterSetPanel extends BorderPane {
private static final Image TICK = new Image("org/sleuthkit/autopsy/timeline/images/tick.png"); //NON-NLS
@FXML
private Button applyButton;
@FXML
private Button defaultButton;
@FXML
private TreeTableView<Filter> filterTreeTable;
@FXML
private TreeTableColumn<AbstractFilter, AbstractFilter> treeColumn;
@FXML
private TreeTableColumn<AbstractFilter, AbstractFilter> legendColumn;
@FXML
private ListView<DescriptionFilter> hiddenDescriptionsListView;
@FXML
private TitledPane hiddenDescriptionsPane;
@FXML
private SplitPane splitPane;
private final FilteredEventsModel filteredEvents;
private final TimeLineController controller;
/**
* 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<Filter, Boolean> expansionMap = FXCollections.observableHashMap();
private double dividerPosition;
@NbBundle.Messages({
"FilterSetPanel.defaultButton.text=Default",
"FilsetSetPanel.hiddenDescriptionsPane.displayName=Hidden Descriptions"})
@FXML
void initialize() {
assert applyButton != null : "fx:id=\"applyButton\" was not injected: check your FXML file 'FilterSetPanel.fxml'."; // NON-NLS
ActionUtils.configureButton(new ApplyFiltersAction(), applyButton);
ActionUtils.configureButton(new ResetFilters(Bundle.FilterSetPanel_defaultButton_text(), controller), defaultButton);
hiddenDescriptionsPane.setText(Bundle.FilsetSetPanel_hiddenDescriptionsPane_displayName());
//remove column headers via css.
filterTreeTable.getStylesheets().addAll(FilterSetPanel.class.getResource("FilterTable.css").toExternalForm()); // NON-NLS
//use row factory as hook to attach context menus to.
filterTreeTable.setRowFactory(ttv -> new FilterTreeTableRow());
//configure tree column to show name of filter and checkbox
treeColumn.setCellValueFactory(cellDataFeatures -> cellDataFeatures.getValue().valueProperty());
treeColumn.setCellFactory(col -> new FilterCheckBoxCellFactory<>().forTreeTable(col));
//configure legend column to show legend (or othe supplamantal ui, eg, text field for text filter)
legendColumn.setCellValueFactory(cellDataFeatures -> cellDataFeatures.getValue().valueProperty());
legendColumn.setCellFactory(col -> new LegendCell(this.controller));
//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());
this.filteredEvents.timeRangeProperty().addListener((Observable observable2) -> applyFilters());
this.filteredEvents.filterProperty().addListener((Observable o) -> refresh());
refresh();
hiddenDescriptionsListView.setItems(controller.getQuickHideFilters());
hiddenDescriptionsListView.setCellFactory(listView -> getNewDiscriptionFilterListCell());
//show and hide the "hidden descriptions" panel depending on the current view mode
controller.viewModeProperty().addListener(observable -> {
applyFilters();
switch (controller.getViewMode()) {
case COUNTS:
case LIST:
//hide for counts and lists, but remember divider position
dividerPosition = splitPane.getDividerPositions()[0];
splitPane.setDividerPositions(1);
hiddenDescriptionsPane.setExpanded(false);
hiddenDescriptionsPane.setCollapsible(false);
hiddenDescriptionsPane.setDisable(true);
break;
case DETAIL:
//show and restore divider position.
splitPane.setDividerPositions(dividerPosition);
hiddenDescriptionsPane.setDisable(false);
hiddenDescriptionsPane.setCollapsible(true);
hiddenDescriptionsPane.setExpanded(true);
hiddenDescriptionsPane.setCollapsible(false);
break;
default:
throw new UnsupportedOperationException("Unknown ViewMode: " + controller.getViewMode());
}
});
}
public FilterSetPanel(TimeLineController controller) {
this.controller = controller;
this.filteredEvents = controller.getEventsModel();
FXMLConstructor.construct(this, "FilterSetPanel.fxml"); // NON-NLS
}
private void refresh() {
Platform.runLater(() -> {
filterTreeTable.setRoot(new FilterTreeItem(filteredEvents.getFilter().copyOf(), expansionMap));
});
}
private void applyFilters() {
Platform.runLater(() -> {
controller.pushFilters((RootFilter) filterTreeTable.getRoot().getValue());
});
}
private ListCell<DescriptionFilter> getNewDiscriptionFilterListCell() {
final ListCell<DescriptionFilter> cell = new FilterCheckBoxCellFactory<DescriptionFilter>().forList();
cell.itemProperty().addListener(itemProperty -> {
if (cell.getItem() == null) {
cell.setContextMenu(null);
} else {
cell.setContextMenu(ActionUtils.createContextMenu(Arrays.asList(
new RemoveDescriptionFilterAction(controller, cell))
));
}
});
return cell;
}
@NbBundle.Messages({"FilterSetPanel.applyButton.text=Apply",
"FilterSetPanel.applyButton.longText=(Re)Apply filters"})
private class ApplyFiltersAction extends Action {
ApplyFiltersAction() {
super(Bundle.FilterSetPanel_applyButton_text());
setLongText(Bundle.FilterSetPanel_applyButton_longText());
setGraphic(new ImageView(TICK));
setEventHandler(actionEvent -> applyFilters());
}
}
@NbBundle.Messages({
"FilterSetPanel.hiddenDescriptionsListView.unhideAndRemove=Unhide and remove from list",
"FilterSetPanel.hiddenDescriptionsListView.remove=Remove from list",})
private static class RemoveDescriptionFilterAction extends Action {
private static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS
RemoveDescriptionFilterAction(TimeLineController controller, Cell<DescriptionFilter> cell) {
super(actionEvent -> controller.getQuickHideFilters().remove(cell.getItem()));
setGraphic(new ImageView(SHOW));
textProperty().bind(
Bindings.when(cell.getItem().selectedProperty())
.then(Bundle.FilterSetPanel_hiddenDescriptionsListView_unhideAndRemove())
.otherwise(Bundle.FilterSetPanel_hiddenDescriptionsListView_remove()));
}
}
}