diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index af1a66247c..0cb66c9bea 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -30,6 +30,7 @@ import java.beans.PropertyVetoException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -45,8 +46,10 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableColumnModelListener; import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import org.netbeans.swing.etable.ETableColumn; +import org.netbeans.swing.etable.ETableColumnModel; import org.netbeans.swing.outline.DefaultOutlineCellRenderer; import org.netbeans.swing.outline.DefaultOutlineModel; import org.netbeans.swing.outline.Outline; @@ -82,7 +85,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { @NbBundle.Messages("DataResultViewerTable.pleasewaitNodeDisplayName=Please Wait...") private static final String PLEASEWAIT_NODE_DISPLAY_NAME = Bundle.DataResultViewerTable_pleasewaitNodeDisplayName(); private static final Color TAGGED_COLOR = new Color(200, 210, 220); - /* + /** * The properties map: * * stored value of column index -> property at that index @@ -92,6 +95,14 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * current table view due to its collection of its children's properties. */ private final Map> propertiesMap = new TreeMap<>(); + + /** + * Stores references to the actual table column objects, keyed by column + * name, so that we can check there visibility later in + * storeColumnVisibility(). + */ + private final Map columnMap = new HashMap<>(); + private final PleasewaitNodeListener pleasewaitNodeListener = new PleasewaitNodeListener(); private Node currentRoot; @@ -101,6 +112,11 @@ public class DataResultViewerTable extends AbstractDataResultViewer { */ private Outline outline; + /** + * Listener for table model event and mouse clicks. + */ + private TableListener tableListener; + /** * Creates a DataResultViewerTable object that is compatible with node * multiple selection actions. @@ -132,7 +148,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { outline.setDefaultRenderer(Object.class, new ColorTagCustomRenderer()); // add a listener so that when columns are moved, the new order is stored - TableListener tableListener = new TableListener(); + tableListener = new TableListener(); outline.getColumnModel().addColumnModelListener(tableListener); // the listener also moves columns back if user tries to move the first column out of place outline.getTableHeader().addMouseListener(tableListener); @@ -144,7 +160,6 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * @param n Node to expand */ @Override - public void expandNode(Node n) { super.expandNode(n); @@ -218,6 +233,12 @@ public class DataResultViewerTable extends AbstractDataResultViewer { Node emptyNode = new AbstractNode(Children.LEAF); em.setRootContext(emptyNode); // make empty node outline.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); + + /* + * Since we are modifying the columns, we don't want to listen + * to added/removed events as un-hide/hide. + */ + tableListener.listenToVisibilityChanges(false); outlineView.setPropertyColumns(); // set the empty property header } } finally { @@ -227,9 +248,15 @@ public class DataResultViewerTable extends AbstractDataResultViewer { /** * Create Column Headers based on the Content represented by the Nodes in - * the table. + * the table. Load persisted column order, sorting and visibility. */ private void setupTable() { + /* + * Since we are modifying the columns, we don't want to listen to + * added/removed events as un-hide/hide, until the table setup is done. + */ + tableListener.listenToVisibilityChanges(false); + /** * OutlineView makes the first column be the result of * node.getDisplayName with the icon. This duplicates our first column, @@ -254,33 +281,66 @@ public class DataResultViewerTable extends AbstractDataResultViewer { */ outline.setAutoResizeMode((props.isEmpty()) ? JTable.AUTO_RESIZE_ALL_COLUMNS : JTable.AUTO_RESIZE_OFF); - assignColumns(props); + assignColumns(props); // assign columns to match the properties setColumnWidths(); + + //Load column sorting information from preferences file and apply it to columns. loadColumnSorting(); - /** + /* + * Save references to columns before we deal with their visibility. This + * has to happen after the sorting is applied, because that actually + * causes the columns to be recreated. It has to happen before + * loadColumnVisibility so we have referenecs to the columns to pass to + * setColumnHidden. + */ + populateColumnMap(); + + //Load column visibility information from preferences file and apply it to columns. + loadColumnVisibility(); + + /* * If one of the child nodes of the root node is to be selected, select * it. */ - SwingUtilities.invokeLater(()->{ - if (currentRoot instanceof TableFilterNode) { - NodeSelectionInfo selectedChildInfo = ((TableFilterNode) currentRoot).getChildNodeSelectionInfo(); - if (null != selectedChildInfo) { - Node[] childNodes = currentRoot.getChildren().getNodes(true); - for (int i = 0; i < childNodes.length; ++i) { - Node childNode = childNodes[i]; - if (selectedChildInfo.matches(childNode)) { - try { - em.setSelectedNodes(new Node[]{childNode}); - } catch (PropertyVetoException ex) { - logger.log(Level.SEVERE, "Failed to select node specified by selected child info", ex); + SwingUtilities.invokeLater(() -> { + if (currentRoot instanceof TableFilterNode) { + NodeSelectionInfo selectedChildInfo = ((TableFilterNode) currentRoot).getChildNodeSelectionInfo(); + if (null != selectedChildInfo) { + Node[] childNodes = currentRoot.getChildren().getNodes(true); + for (int i = 0; i < childNodes.length; ++i) { + Node childNode = childNodes[i]; + if (selectedChildInfo.matches(childNode)) { + try { + em.setSelectedNodes(new Node[]{childNode}); + } catch (PropertyVetoException ex) { + logger.log(Level.SEVERE, "Failed to select node specified by selected child info", ex); + } + break; } - break; } + ((TableFilterNode) currentRoot).setChildNodeSelectionInfo(null); } - ((TableFilterNode) currentRoot).setChildNodeSelectionInfo(null); } - }}); + }); + + //the table setup is done, so any added/removed events can now be treated as un-hide/hide. + tableListener.listenToVisibilityChanges(true); + } + + /* + * Populate the map with references to the column objects for use when + * loading/storing the visibility info. + */ + private void populateColumnMap() { + columnMap.clear(); + TableColumnModel columnModel = outline.getColumnModel(); + //for each property get a reference to the column object from the column model. + for (Map.Entry> entry : propertiesMap.entrySet()) { + final String propName = entry.getValue().getName(); + final ETableColumn column = (ETableColumn) columnModel.getColumn(entry.getKey()); + columnMap.put(propName, column); + } } private void setColumnWidths() { @@ -320,7 +380,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } } - private void assignColumns(List> props) { + synchronized private void assignColumns(List> props) { // Get the columns setup with respect to names and sortability String[] propStrings = new String[props.size() * 2]; for (int i = 0; i < props.size(); i++) { @@ -339,29 +399,71 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } /** - * Store the current column order and sort information into a preference - * file. + * Store the current column visibility information into a preference file. */ - private synchronized void storeColumnState() { + private synchronized void storeColumnVisibility() { if (currentRoot == null || propertiesMap.isEmpty()) { return; } if (currentRoot instanceof TableFilterNode) { TableFilterNode tfn = (TableFilterNode) currentRoot; final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); + final ETableColumnModel columnModel = (ETableColumnModel) outline.getColumnModel(); + + //store hidden state + for (Map.Entry entry : columnMap.entrySet()) { + + String columnName = entry.getKey(); + final String columnHiddenKey = ResultViewerPersistence.getColumnHiddenKey(tfn, columnName); + final TableColumn column = entry.getValue(); + + boolean columnHidden = columnModel.isColumnHidden(column); + if (columnHidden) { + preferences.putBoolean(columnHiddenKey, true); + } else { + preferences.remove(columnHiddenKey); + } + } + } + } + + /** + * Store the current column order information into a preference file. + */ + private synchronized void storeColumnOrder() { + if (currentRoot == null || propertiesMap.isEmpty()) { + return; + } + if (currentRoot instanceof TableFilterNode) { + TableFilterNode tfn = (TableFilterNode) currentRoot; + final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); + // Store the current order of the columns into settings for (Map.Entry> entry : propertiesMap.entrySet()) { preferences.putInt(ResultViewerPersistence.getColumnPositionKey(tfn, entry.getValue().getName()), entry.getKey()); } - final TableColumnModel columnModel = outline.getColumnModel(); + } + } - //store the sorting information - int numCols = columnModel.getColumnCount(); - for (int i = 0; i < numCols; i++) { - ETableColumn etc = (ETableColumn) columnModel.getColumn(i); - String columnName = outline.getColumnName(i); + /** + * Store the current column sorting information into a preference file. + */ + private synchronized void storeColumnSorting() { + if (currentRoot == null || propertiesMap.isEmpty()) { + return; + } + if (currentRoot instanceof TableFilterNode) { + final TableFilterNode tfn = ((TableFilterNode) currentRoot); + final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); + + for (Map.Entry entry : columnMap.entrySet()) { + ETableColumn etc = entry.getValue(); + String columnName = entry.getKey(); + + //store sort rank and order final String columnSortOrderKey = ResultViewerPersistence.getColumnSortOrderKey(tfn, columnName); final String columnSortRankKey = ResultViewerPersistence.getColumnSortRankKey(tfn, columnName); + if (etc.isSorted()) { preferences.putBoolean(columnSortOrderKey, etc.isAscending()); preferences.putInt(columnSortRankKey, etc.getSortRank()); @@ -370,13 +472,14 @@ public class DataResultViewerTable extends AbstractDataResultViewer { preferences.remove(columnSortRankKey); } } + } } /** * Reads and applies the column sorting information persisted to the * preferences file. Must be called after loadColumnOrder() since it depends - * on propertiesMap being initialized, and before assignColumns since it + * on propertiesMap being initialized, and after assignColumns since it * cannot set the sort on columns that have not been added to the table. */ private synchronized void loadColumnSorting() { @@ -393,6 +496,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { propertiesMap.entrySet().stream().forEach(entry -> { final String propName = entry.getValue().getName(); //if the sort rank is undefined, it will be defaulted to 0 => unsorted. + Integer sortRank = preferences.getInt(ResultViewerPersistence.getColumnSortRankKey(tfn, propName), 0); //default to true => ascending Boolean sortOrder = preferences.getBoolean(ResultViewerPersistence.getColumnSortOrderKey(tfn, propName), true); @@ -405,6 +509,26 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } } + private synchronized void loadColumnVisibility() { + if (currentRoot == null || propertiesMap.isEmpty()) { + return; + } + + if (currentRoot instanceof TableFilterNode) { + + final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); + + final TableFilterNode tfn = ((TableFilterNode) currentRoot); + ETableColumnModel columnModel = (ETableColumnModel) outline.getColumnModel(); + for (Map.Entry> entry : propertiesMap.entrySet()) { + final String propName = entry.getValue().getName(); + boolean hidden = preferences.getBoolean(ResultViewerPersistence.getColumnHiddenKey(tfn, propName), false); + final TableColumn column = columnMap.get(propName); + columnModel.setColumnHidden(column, hidden); + } + } + } + /** * Gets a list of child properties (columns) in the order persisted in the * preference file. Also initialized the propertiesMap with the column @@ -437,6 +561,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); for (Property prop : props) { + Integer value = preferences.getInt(ResultViewerPersistence.getColumnPositionKey(tfn, prop.getName()), -1); if (value >= 0 && value < offset && !propertiesMap.containsKey(value)) { propertiesMap.put(value, prop); @@ -501,7 +626,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { /** * Listens to mouse events and table column events and persists column order - * and sorting changes + * sorting, and visibility changes. */ private class TableListener extends MouseAdapter implements TableColumnModelListener { @@ -509,6 +634,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { // the column started and where it ended up. private int startColumnIndex = -1; private int endColumnIndex = -1; + private boolean listenToVisibilitEvents; @Override public void columnMoved(TableColumnModelEvent e) { @@ -560,7 +686,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { propertiesMap.put(range.get(0), movedProp); } - storeColumnState(); + storeColumnOrder(); } @Override @@ -584,15 +710,30 @@ public class DataResultViewerTable extends AbstractDataResultViewer { @Override public void mouseClicked(MouseEvent e) { - storeColumnState(); + //the user clicked a column header + storeColumnSorting(); } @Override public void columnAdded(TableColumnModelEvent e) { + columnAddedOrRemoved(); } @Override public void columnRemoved(TableColumnModelEvent e) { + columnAddedOrRemoved(); + } + + /** + * Process a columnAdded or columnRemoved event. If we are listening to + * visibilty events the assumption is that added/removed are really + * unhide/hide. If we are not listening do nothing. + */ + private void columnAddedOrRemoved() { + if (listenToVisibilitEvents) { + SwingUtilities.invokeLater(DataResultViewerTable.this::storeColumnVisibility); + + } } @Override @@ -602,6 +743,19 @@ public class DataResultViewerTable extends AbstractDataResultViewer { @Override public void columnSelectionChanged(ListSelectionEvent e) { } + + /** + * Set the listener to listen or not to visibility changes. When this is + * true, the listener treats all column added/removed events as + * un-hide/hide, and persists the hidden/visible state to the + * preferences file. When false, the listener treats added/removed as + * added/removed (which it ignores). + * + * @param b + */ + private void listenToVisibilityChanges(boolean b) { + this.listenToVisibilitEvents = b; + } } private class PleasewaitNodeListener extends NodeAdapter { diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ResultViewerPersistence.java b/Core/src/org/sleuthkit/autopsy/corecomponents/ResultViewerPersistence.java index 2102f320be..148d3aca74 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ResultViewerPersistence.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ResultViewerPersistence.java @@ -73,6 +73,19 @@ final class ResultViewerPersistence { return getColumnKeyBase(node, propName) + ".sortRank"; } + /** + * Gets a key for the given node and a property of its child nodes to store + * the visibility into a preference file. + * + * @param node The node whose type will be used to generate the key + * @param propName The property used to generate the key. + * + * @return A generated key for the preference file + */ + static String getColumnHiddenKey(TableFilterNode node, String propName) { + return getColumnKeyBase(node, propName) + ".hidden"; + } + private static String getColumnKeyBase(TableFilterNode node, String propName) { return stripNonAlphanumeric(node.getColumnOrderKey()) + "." + stripNonAlphanumeric(propName); }