diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties index 183a66a263..14b3c47fa7 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties @@ -90,11 +90,8 @@ DataContentViewerString.setDataView.errorNoText=(offset {0}-{1} contains no text DataContentViewerString.title=Strings DataContentViewerString.toolTip=Displays ASCII and Unicode strings extracted from the file. DataResultPanel.pleasewaitNodeDisplayName=Please Wait... -DataResultViewerTable.firstColLbl=Name DataResultViewerTable.illegalArgExc.noChildFromParent=Could not get a child Node from the given parent. DataResultViewerTable.illegalArgExc.childWithoutPropertySet=Child Node does not have the regular PropertySet. -DataResultViewerTable.title=Table -DataResultViewerTable.pleasewaitNodeDisplayName=Please Wait... DataResultViewerThumbnail.title=Thumbnail DataResultViewerThumbnail.goToPageTextField.msgDlg=Please enter a valid page number between 1 and {0} DataResultViewerThumbnail.goToPageTextField.err=Invalid page number diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index 92191d15a7..b42dc6ed58 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2016 Basis Technology Corp. + * Copyright 2011-2017 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,6 +37,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.prefs.Preferences; +import java.util.stream.Stream; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; @@ -46,6 +47,7 @@ import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableColumnModelListener; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; +import org.apache.commons.lang3.StringUtils; import org.netbeans.swing.etable.ETableColumn; import org.netbeans.swing.outline.DefaultOutlineCellRenderer; import org.netbeans.swing.outline.DefaultOutlineModel; @@ -62,37 +64,41 @@ import org.openide.nodes.NodeMemberEvent; import org.openide.util.NbBundle; import org.openide.util.NbPreferences; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; /** * DataResult sortable table viewer + * + * @@@ Restore implementation of DataResultViewerTable as a DataResultViewer + * service provider when DataResultViewers can be made compatible with node + * multiple selection actions. */ -// @@@ Restore implementation of DataResultViewerTable as a DataResultViewer -// service provider when DataResultViewers can be made compatible with node -// multiple selection actions. //@ServiceProvider(service = DataResultViewer.class) public class DataResultViewerTable extends AbstractDataResultViewer { private static final long serialVersionUID = 1L; - static private final String FIRST_COLUMN_LABEL = NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.firstColLbl"); + @NbBundle.Messages("DataResultViewerTable.firstColLbl=Name") + static private final String FIRST_COLUMN_LABEL = Bundle.DataResultViewerTable_firstColLbl(); + @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 maps key: stored value of column index -> value: - * property at that index We move around stored values instead of directly - * using the column indices in order to not override settings for a column - * that may not appear in the current table view due to its collection of - * its children's properties. + * The properties map: + * + * stored value of column index -> property at that index + * + * We move around stored values instead of directly using the column indices + * in order to not override settings for a column that may not appear in the + * current table view due to its collection of its children's properties. */ private final Map> propertiesMap = new TreeMap<>(); private final PleasewaitNodeListener pleasewaitNodeListener = new PleasewaitNodeListener(); - private static final String PLEASEWAIT_NODE_DISPLAY_NAME = NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.pleasewaitNodeDisplayName"); - private static final Color TAGGED_COLOR = new Color(200, 210, 220); + private Node currentRoot; - // When a column in the table is moved, these two variables keep track of where - // the column started and where it ended up. - private int startColumnIndex = -1; - private int endColumnIndex = -1; - /** - * Convenience reference to internal Outline + + /* + * Convience reference to internal Outline. */ private Outline outline; @@ -119,114 +125,18 @@ public class DataResultViewerTable extends AbstractDataResultViewer { initComponents(); outlineView.setAllowedDragActions(DnDConstants.ACTION_NONE); + outline = outlineView.getOutline(); outline.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - outline.setRootVisible(false); + outline.setRootVisible(false); // don't show the root node outline.setDragEnabled(false); + outline.setDefaultRenderer(Object.class, new ColorTagCustomRenderer()); // add a listener so that when columns are moved, the new order is stored - outline.getColumnModel().addColumnModelListener(new TableColumnModelListener() { - - @Override - public void columnMoved(TableColumnModelEvent e) { - int fromIndex = e.getFromIndex(); - int toIndex = e.getToIndex(); - if (fromIndex == toIndex) { - return; - } - - /* - * Because a column may be dragged to several different - * positions before the mouse is released (thus causing multiple - * TableColumnModelEvents to be fired), we want to keep track of - * the starting column index in this potential series of - * movements. Therefore we only keep track of the original - * fromIndex in startColumnIndex, but we always update - * endColumnIndex to know the final position of the moved - * column. See the MouseListener mouseReleased method. - */ - if (startColumnIndex == -1) { - startColumnIndex = fromIndex; - } - endColumnIndex = toIndex; - - // This array contains the keys of propertiesMap in order - int[] indicesList = new int[propertiesMap.size()]; - int pos = 0; - for (int key : propertiesMap.keySet()) { - indicesList[pos++] = key; - } - int leftIndex = Math.min(fromIndex, toIndex); - int rightIndex = Math.max(fromIndex, toIndex); - // Now we can copy the range of keys that have been affected by - // the column movement - int[] range = Arrays.copyOfRange(indicesList, leftIndex, rightIndex + 1); - int rangeSize = range.length; - - // column moved right, shift all properties left, put in moved - // property at the rightmost index - if (fromIndex < toIndex) { - Property movedProp = propertiesMap.get(range[0]); - for (int i = 0; i < rangeSize - 1; i++) { - propertiesMap.put(range[i], propertiesMap.get(range[i + 1])); - } - propertiesMap.put(range[rangeSize - 1], movedProp); - } // column moved left, shift all properties right, put in moved - // property at the leftmost index - else { - Property movedProp = propertiesMap.get(range[rangeSize - 1]); - for (int i = rangeSize - 1; i > 0; i--) { - propertiesMap.put(range[i], propertiesMap.get(range[i - 1])); - } - propertiesMap.put(range[0], movedProp); - } - - storeState(); - } - - @Override - public void columnAdded(TableColumnModelEvent e) { - } - - @Override - public void columnRemoved(TableColumnModelEvent e) { - } - - @Override - public void columnMarginChanged(ChangeEvent e) { - } - - @Override - public void columnSelectionChanged(ListSelectionEvent e) { - } - }); - - // add a listener to move columns back if user tries to move the first column out of place - outline.getTableHeader().addMouseListener(new MouseAdapter() { - @Override - public void mouseReleased(MouseEvent e) { - /* - * If the startColumnIndex is not -1 (which is the reset value), - * that means columns have been moved around. We then check to - * see if either the starting or end position is 0 (the first - * column), and then swap them back if that is the case because - * we don't want to allow movement of the first column. We then - * reset startColumnIndex to -1, the reset value. We check if - * startColumnIndex is at reset or not because it is possible - * for the mouse to be released and a MouseEvent to be fired - * without having moved any columns. - */ - if (startColumnIndex != -1 && (startColumnIndex == 0 || endColumnIndex == 0)) { - outline.moveColumn(endColumnIndex, startColumnIndex); - } - startColumnIndex = -1; - } - - @Override - public void mouseClicked(MouseEvent e) { - storeState(); - } - }); + 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); } /** @@ -235,6 +145,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * @param n Node to expand */ @Override + public void expandNode(Node n) { super.expandNode(n); @@ -263,40 +174,31 @@ public class DataResultViewerTable extends AbstractDataResultViewer { .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 366, Short.MAX_VALUE) ); }// //GEN-END:initComponents - - // Variables declaration - do not modify//GEN-BEGIN:variables private org.openide.explorer.view.OutlineView outlineView; // End of variables declaration//GEN-END:variables /** - * Gets regular Bean property set properties from all children and, - * recursively, subchildren, of a Node. + * Get the properties from all children and, recursively, subchildren of + * Node. Note: won't work out the box for lazy load - you need to set all + * children props for the parent by hand * - * Note: won't work out the box for lazy load - you need to set all children - * properties for the parent by hand. - * - * @param parent Node (with at least one child) from which toget - * properties. - * @param rows Maximum number of rows to retrieve properties for - * (can be used for memory optimization). - * @param propertiesAcc Set in which to accumulate the properties. + * @param parent Node with at least one child to get properties from + * @param maxRows max number of rows to retrieve properties for (can + * be used for memory optimization) + * @param propertiesAcc Accumulator for properties. */ - private void getAllChildPropertyHeadersRec(Node parent, int rows, Set> propertiesAcc) { + static private void getAllChildProperties(Node parent, int maxRows, Set> propertiesAcc) { Children children = parent.getChildren(); int childCount = 0; for (Node child : children.getNodes()) { - if (++childCount > rows) { + if (++childCount > maxRows) { return; } for (PropertySet ps : child.getPropertySets()) { - final Property[] props = ps.getProperties(); - final int propsNum = props.length; - for (int j = 0; j < propsNum; ++j) { - propertiesAcc.add(props[j]); - } + propertiesAcc.addAll(Arrays.asList(ps.getProperties())); } - getAllChildPropertyHeadersRec(child, rows, propertiesAcc); + getAllChildProperties(child, maxRows, propertiesAcc); } } @@ -305,13 +207,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer { return true; } - /** - * Thread note: Make sure to run this in the EDT as it causes GUI - * operations. - * - * @param selectedNode - */ @Override + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public void setNode(Node selectedNode) { /* @@ -377,33 +274,26 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * duplicates getDisplayName(). The current implementation does not * allow the first property column to be moved. */ - if (props.size() > 0) { + if (props.isEmpty() == false) { Node.Property prop = props.remove(0); ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(prop.getDisplayName()); } + /* + * show the horizontal scroll panel and show all the content & header If + * there is only one column (which was removed from props above) Just + * let the table resize itself. + */ + outline.setAutoResizeMode((props.isEmpty()) ? JTable.AUTO_RESIZE_ALL_COLUMNS : JTable.AUTO_RESIZE_OFF); - // 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++) { - final Property prop = props.get(i); - prop.setValue("ComparableColumnTTV", Boolean.TRUE); //NON-NLS - //First property column is sorted initially - if (i == 0) { - prop.setValue("TreeColumnTTV", Boolean.TRUE); // Identifies special property representing first (tree) column. NON-NLS - prop.setValue("SortingColumnTTV", Boolean.TRUE); // TreeTableView should be initially sorted by this property column. NON-NLS - } - propStrings[2 * i] = prop.getName(); - propStrings[2 * i + 1] = prop.getDisplayName(); - } + assignColumns(props); - outlineView.setPropertyColumns(propStrings); + setColumnWidths(); - // show the horizontal scroll panel and show all the content & header - // If there is only one column (which was removed from props above) - // Just let the table resize itself. - outline.setAutoResizeMode((props.size() > 0) ? JTable.AUTO_RESIZE_OFF : JTable.AUTO_RESIZE_ALL_COLUMNS); + loadColumnSorting(); + } - if (root.getChildren().getNodesCount() != 0) { + private void setColumnWidths() { + if (currentRoot.getChildren().getNodesCount() != 0) { final Graphics graphics = outlineView.getGraphics(); if (graphics != null) { final FontMetrics metrics = graphics.getFontMetrics(); @@ -437,58 +327,31 @@ public class DataResultViewerTable extends AbstractDataResultViewer { // if there's no content just auto resize all columns outline.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); } + } - loadSort(); - - /** - * This custom renderer extends the renderer that was already being used - * by the outline table. This renderer colors a row if the tags property - * of the node is not empty. - */ - class ColorTagCustomRenderer extends DefaultOutlineCellRenderer { - - private static final long serialVersionUID = 1L; - - @Override - public Component getTableCellRendererComponent(JTable table, - Object value, boolean isSelected, boolean hasFocus, int row, int col) { - - Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col); - // only override the color if a node is not selected - if (!isSelected) { - Node node = currentRoot.getChildren().getNodeAt(table.convertRowIndexToModel(row)); - boolean tagFound = false; - if (node != null) { - Node.PropertySet[] propSets = node.getPropertySets(); - if (propSets.length != 0) { - // currently, a node has only one property set, named Sheet.PROPERTIES ("properties") - Node.Property[] props = propSets[0].getProperties(); - for (Property prop : props) { - if (prop.getName().equals("Tags")) { - try { - tagFound = !prop.getValue().equals(""); - } catch (IllegalAccessException | InvocationTargetException ignore) { - } - break; - } - } - } - } - //if the node does have associated tags, set its background color - if (tagFound) { - component.setBackground(TAGGED_COLOR); - } - } - return component; + 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++) { + final Property prop = props.get(i); + prop.setValue("ComparableColumnTTV", Boolean.TRUE); //NON-NLS + //First property column is sorted initially + if (i == 0) { + prop.setValue("TreeColumnTTV", Boolean.TRUE); // Identifies special property representing first (tree) column. NON-NLS + prop.setValue("SortingColumnTTV", Boolean.TRUE); // TreeTableView should be initially sorted by this property column. NON-NLS } + propStrings[2 * i] = prop.getName(); + propStrings[2 * i + 1] = prop.getDisplayName(); } - outline.setDefaultRenderer(Object.class, new ColorTagCustomRenderer()); + + outlineView.setPropertyColumns(propStrings); } /** - * Store the current column order into a preference file. + * Store the current column order and sort information into a preference + * file. */ - private synchronized void storeState() { + private synchronized void storeColumnState() { if (currentRoot == null || propertiesMap.isEmpty()) { return; } @@ -517,24 +380,13 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } } - static private final class ColumnSortInfo { - - private final int modelIndex; - private final int rank; - private final boolean order; - - private ColumnSortInfo(int modelIndex, int rank, boolean order) { - this.modelIndex = modelIndex; - this.rank = rank; - this.order = order; - } - - private int getRank() { - return rank; - } - } - - private synchronized void loadSort() { + /** + * 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 + * cannot set the sort on columns that have not been added to the table. + */ + private synchronized void loadColumnSorting() { if (currentRoot == null || propertiesMap.isEmpty()) { return; } @@ -543,48 +395,45 @@ public class DataResultViewerTable extends AbstractDataResultViewer { final TableFilterNode tfn = (TableFilterNode) currentRoot; final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); - //organize property sorting information, sorted by rank TreeSet treeSet = new TreeSet<>(Comparator.comparing(ColumnSortInfo::getRank)); - 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 = Integer.valueOf(preferences.get(ResultViewerPersistence.getColumnSortRankKey(tfn, propName), "0")); - //default to true => ascending - Boolean sortOrder = Boolean.valueOf(preferences.get(ResultViewerPersistence.getColumnSortOrderKey(tfn, propName), "true")); + 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 = Integer.valueOf(preferences.get(ResultViewerPersistence.getColumnSortRankKey(tfn, propName), "0")); + //default to true => ascending + Boolean sortOrder = Boolean.valueOf(preferences.get(ResultViewerPersistence.getColumnSortOrderKey(tfn, propName), "true")); - treeSet.add(new ColumnSortInfo(entry.getKey(), sortRank, sortOrder)); - }); + treeSet.add(new ColumnSortInfo(entry.getKey(), sortRank, sortOrder)); + }); //apply sort information in rank order. treeSet.forEach(sortInfo -> outline.setColumnSorted(sortInfo.modelIndex, sortInfo.order, sortInfo.rank)); } - } /** - * Loads the stored column order from the preference file. + * Gets a list of child properties (columns) in the order persisted in the + * preference file. Also initialized the propertiesMap with the column + * order. * - * @return a List> of the preferences in order + * @return a List> of the properties in the persisted + * order. */ private synchronized List> loadColumnOrder() { - // This is a set because we add properties of up to 100 child nodes, and we want unique properties + // This is a set because we only want unique properties Set> propertiesAcc = new LinkedHashSet<>(); - this.getAllChildPropertyHeadersRec(currentRoot, 100, propertiesAcc); - - List> props = new ArrayList<>(propertiesAcc); + getAllChildProperties(currentRoot, 100, propertiesAcc); + List> props = new ArrayList<>(propertiesAcc); // If node is not table filter node, use default order for columns - TableFilterNode tfn; - if (currentRoot instanceof TableFilterNode) { - tfn = (TableFilterNode) currentRoot; - } else { - // The node is not a TableFilterNode, columns are going to be in default order + if (!(currentRoot instanceof TableFilterNode)) { return props; } + final TableFilterNode tfn = ((TableFilterNode) currentRoot); propertiesMap.clear(); + /* * We load column index values into the properties map. If a property's * index is outside the range of the number of properties or the index @@ -610,20 +459,19 @@ public class DataResultViewerTable extends AbstractDataResultViewer { // If none of the properties had previous settings, we should decrement // each value by the number of properties to make the values 0-indexed. if (noPreviousSettings) { - Integer[] keys = propertiesMap.keySet().toArray(new Integer[propertiesMap.keySet().size()]); + ArrayList keys = new ArrayList<>(propertiesMap.keySet()); for (int key : keys) { - propertiesMap.put(key - props.size(), propertiesMap.get(key)); - propertiesMap.remove(key); + propertiesMap.put(key - props.size(), propertiesMap.remove(key)); } } return new ArrayList<>(propertiesMap.values()); - } @Override + @NbBundle.Messages("DataResultViewerTable.title=Table") public String getTitle() { - return NbBundle.getMessage(this.getClass(), "DataResultViewerTable.title"); + return Bundle.DataResultViewerTable_title(); } @Override @@ -637,6 +485,132 @@ public class DataResultViewerTable extends AbstractDataResultViewer { this.outlineView = null; super.clearComponent(); + + } + + /** + * Encapsulates sorting information for a column to make loadSort simpler. + */ + static private final class ColumnSortInfo { + + private final int modelIndex; + private final int rank; + private final boolean order; + + private ColumnSortInfo(int modelIndex, int rank, boolean order) { + this.modelIndex = modelIndex; + this.rank = rank; + this.order = order; + } + + private int getRank() { + return rank; + } + } + + /** + * Listens to mouse events and table column events and persists column order + * and sorting changes + */ + private class TableListener extends MouseAdapter implements TableColumnModelListener { + + // When a column in the table is moved, these two variables keep track of where + // the column started and where it ended up. + private int startColumnIndex = -1; + private int endColumnIndex = -1; + + @Override + public void columnMoved(TableColumnModelEvent e) { + int fromIndex = e.getFromIndex(); + int toIndex = e.getToIndex(); + if (fromIndex == toIndex) { + return; + } + + /* + * Because a column may be dragged to several different positions + * before the mouse is released (thus causing multiple + * TableColumnModelEvents to be fired), we want to keep track of the + * starting column index in this potential series of movements. + * Therefore we only keep track of the original fromIndex in + * startColumnIndex, but we always update endColumnIndex to know the + * final position of the moved column. See the MouseListener + * mouseReleased method. + */ + if (startColumnIndex == -1) { + startColumnIndex = fromIndex; + } + endColumnIndex = toIndex; + + // This list contains the keys of propertiesMap in order + ArrayList indicesList = new ArrayList<>(propertiesMap.keySet()); + int leftIndex = Math.min(fromIndex, toIndex); + int rightIndex = Math.max(fromIndex, toIndex); + // Now we can copy the range of keys that have been affected by + // the column movement + List range = indicesList.subList(leftIndex, rightIndex + 1); + int rangeSize = range.size(); + + if (fromIndex < toIndex) { + // column moved right, shift all properties left, put in moved + // property at the rightmost index + Property movedProp = propertiesMap.get(range.get(0)); + for (int i = 0; i < rangeSize - 1; i++) { + propertiesMap.put(range.get(i), propertiesMap.get(range.get(i + 1))); + } + propertiesMap.put(range.get(rangeSize - 1), movedProp); + } else { + // column moved left, shift all properties right, put in moved + // property at the leftmost index + Property movedProp = propertiesMap.get(range.get(rangeSize - 1)); + for (int i = rangeSize - 1; i > 0; i--) { + propertiesMap.put(range.get(i), propertiesMap.get(range.get(i - 1))); + } + propertiesMap.put(range.get(0), movedProp); + } + + storeColumnState(); + } + + @Override + public void mouseReleased(MouseEvent e) { + /* + * If the startColumnIndex is not -1 (which is the reset value), + * that means columns have been moved around. We then check to see + * if either the starting or end position is 0 (the first column), + * and then swap them back if that is the case because we don't want + * to allow movement of the first column. We then reset + * startColumnIndex to -1, the reset value. We check if + * startColumnIndex is at reset or not because it is possible for + * the mouse to be released and a MouseEvent to be fired without + * having moved any columns. + */ + if (startColumnIndex != -1 && (startColumnIndex == 0 || endColumnIndex == 0)) { + outline.moveColumn(endColumnIndex, startColumnIndex); + } + startColumnIndex = -1; + } + + @Override + public void mouseClicked(MouseEvent e) { + storeColumnState(); + } + + @Override + public void columnAdded(TableColumnModelEvent e) { + } + + @Override + public void columnRemoved(TableColumnModelEvent e) { + } + + @Override + public void columnMarginChanged(ChangeEvent e) { + } + + @Override + public void columnSelectionChanged(ListSelectionEvent e) { + } } private class PleasewaitNodeListener extends NodeAdapter { @@ -652,23 +626,61 @@ public class DataResultViewerTable extends AbstractDataResultViewer { Node[] delta = nme.getDelta(); if (load && containsReal(delta)) { load = false; + //JMTODO: this looks suspicious if (SwingUtilities.isEventDispatchThread()) { setupTable(nme.getNode()); } else { - SwingUtilities.invokeLater(() -> { - setupTable(nme.getNode()); - }); + SwingUtilities.invokeLater(() -> setupTable(nme.getNode())); } } } private boolean containsReal(Node[] delta) { - for (Node n : delta) { - if (!n.getDisplayName().equals(PLEASEWAIT_NODE_DISPLAY_NAME)) { - return true; + return Stream.of(delta) + .map(Node::getDisplayName) + .noneMatch(PLEASEWAIT_NODE_DISPLAY_NAME::equals); + } + } + + /** + * This custom renderer extends the renderer that was already being used by + * the outline table. This renderer colors a row if the tags property of the + * node is not empty. + */ + private class ColorTagCustomRenderer extends DefaultOutlineCellRenderer { + + private static final long serialVersionUID = 1L; + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { + + Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col); + // only override the color if a node is not selected + if (!isSelected) { + Node node = currentRoot.getChildren().getNodeAt(table.convertRowIndexToModel(row)); + boolean tagFound = false; + if (node != null) { + Node.PropertySet[] propSets = node.getPropertySets(); + if (propSets.length != 0) { + // currently, a node has only one property set, named Sheet.PROPERTIES ("properties") + Node.Property[] props = propSets[0].getProperties(); + for (Property prop : props) { + if ("Tags".equals(prop.getName())) {//NON-NLS + try { + tagFound = !prop.getValue().equals(""); + } catch (IllegalAccessException | InvocationTargetException ignore) { + } + break; + } + } + } + } + //if the node does have associated tags, set its background color + if (tagFound) { + component.setBackground(TAGGED_COLOR); } } - return false; + return component; } } }