Merge remote-tracking branch 'upstream/develop' into 1659-sort-thumbnailviewer

# Conflicts:
#	Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java
This commit is contained in:
millmanorama 2017-05-12 16:51:51 +02:00
commit a3cb94a061
2 changed files with 276 additions and 267 deletions

View File

@ -90,11 +90,8 @@ DataContentViewerString.setDataView.errorNoText=(offset {0}-{1} contains no text
DataContentViewerString.title=Strings DataContentViewerString.title=Strings
DataContentViewerString.toolTip=Displays ASCII and Unicode strings extracted from the file. DataContentViewerString.toolTip=Displays ASCII and Unicode strings extracted from the file.
DataResultPanel.pleasewaitNodeDisplayName=Please Wait... DataResultPanel.pleasewaitNodeDisplayName=Please Wait...
DataResultViewerTable.firstColLbl=Name
DataResultViewerTable.illegalArgExc.noChildFromParent=Could not get a child Node from the given parent. 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.illegalArgExc.childWithoutPropertySet=Child Node does not have the regular PropertySet.
DataResultViewerTable.title=Table
DataResultViewerTable.pleasewaitNodeDisplayName=Please Wait...
DataResultViewerThumbnail.title=Thumbnail DataResultViewerThumbnail.title=Thumbnail
DataResultViewerThumbnail.goToPageTextField.msgDlg=Please enter a valid page number between 1 and {0} DataResultViewerThumbnail.goToPageTextField.msgDlg=Please enter a valid page number between 1 and {0}
DataResultViewerThumbnail.goToPageTextField.err=Invalid page number DataResultViewerThumbnail.goToPageTextField.err=Invalid page number

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-2016 Basis Technology Corp. * Copyright 2011-2017 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org * Contact: carrier <at> sleuthkit <dot> org
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * 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.TreeMap;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import java.util.stream.Stream;
import javax.swing.JTable; import javax.swing.JTable;
import javax.swing.ListSelectionModel; import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
@ -46,6 +47,7 @@ import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener; import javax.swing.event.TableColumnModelListener;
import javax.swing.table.TableCellRenderer; import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel; import javax.swing.table.TableColumnModel;
import org.apache.commons.lang3.StringUtils;
import org.netbeans.swing.etable.ETableColumn; import org.netbeans.swing.etable.ETableColumn;
import org.netbeans.swing.outline.DefaultOutlineCellRenderer; import org.netbeans.swing.outline.DefaultOutlineCellRenderer;
import org.netbeans.swing.outline.DefaultOutlineModel; import org.netbeans.swing.outline.DefaultOutlineModel;
@ -62,37 +64,41 @@ import org.openide.nodes.NodeMemberEvent;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.openide.util.NbPreferences; import org.openide.util.NbPreferences;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
/** /**
* DataResult sortable table viewer * 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) //@ServiceProvider(service = DataResultViewer.class)
public class DataResultViewerTable extends AbstractDataResultViewer { public class DataResultViewerTable extends AbstractDataResultViewer {
private static final long serialVersionUID = 1L; 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: * The properties map:
* 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 * stored value of column index -> property at that index
* that may not appear in the current table view due to its collection of *
* its children's properties. * 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<Integer, Property<?>> propertiesMap = new TreeMap<>(); private final Map<Integer, Property<?>> propertiesMap = new TreeMap<>();
private final PleasewaitNodeListener pleasewaitNodeListener = new PleasewaitNodeListener(); 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; 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; * Convience reference to internal Outline.
private int endColumnIndex = -1;
/**
* Convenience reference to internal Outline
*/ */
private Outline outline; private Outline outline;
@ -119,114 +125,18 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
initComponents(); initComponents();
outlineView.setAllowedDragActions(DnDConstants.ACTION_NONE); outlineView.setAllowedDragActions(DnDConstants.ACTION_NONE);
outline = outlineView.getOutline(); outline = outlineView.getOutline();
outline.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); outline.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
outline.setRootVisible(false); outline.setRootVisible(false); // don't show the root node
outline.setDragEnabled(false); outline.setDragEnabled(false);
outline.setDefaultRenderer(Object.class, new ColorTagCustomRenderer());
// add a listener so that when columns are moved, the new order is stored // add a listener so that when columns are moved, the new order is stored
outline.getColumnModel().addColumnModelListener(new TableColumnModelListener() { TableListener tableListener = new TableListener();
outline.getColumnModel().addColumnModelListener(tableListener);
@Override // the listener also moves columns back if user tries to move the first column out of place
public void columnMoved(TableColumnModelEvent e) { outline.getTableHeader().addMouseListener(tableListener);
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();
}
});
} }
/** /**
@ -235,6 +145,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
* @param n Node to expand * @param n Node to expand
*/ */
@Override @Override
public void expandNode(Node n) { public void expandNode(Node n) {
super.expandNode(n); super.expandNode(n);
@ -263,40 +174,31 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
.addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 366, Short.MAX_VALUE) .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 366, Short.MAX_VALUE)
); );
}// </editor-fold>//GEN-END:initComponents }// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables // Variables declaration - do not modify//GEN-BEGIN:variables
private org.openide.explorer.view.OutlineView outlineView; private org.openide.explorer.view.OutlineView outlineView;
// End of variables declaration//GEN-END:variables // End of variables declaration//GEN-END:variables
/** /**
* Gets regular Bean property set properties from all children and, * Get the properties from all children and, recursively, subchildren of
* recursively, subchildren, of a Node. * 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 * @param parent Node with at least one child to get properties from
* properties for the parent by hand. * @param maxRows max number of rows to retrieve properties for (can
* * be used for memory optimization)
* @param parent Node (with at least one child) from which toget * @param propertiesAcc Accumulator for properties.
* 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.
*/ */
private void getAllChildPropertyHeadersRec(Node parent, int rows, Set<Property<?>> propertiesAcc) { static private void getAllChildProperties(Node parent, int maxRows, Set<Property<?>> propertiesAcc) {
Children children = parent.getChildren(); Children children = parent.getChildren();
int childCount = 0; int childCount = 0;
for (Node child : children.getNodes()) { for (Node child : children.getNodes()) {
if (++childCount > rows) { if (++childCount > maxRows) {
return; return;
} }
for (PropertySet ps : child.getPropertySets()) { for (PropertySet ps : child.getPropertySets()) {
final Property<?>[] props = ps.getProperties(); propertiesAcc.addAll(Arrays.asList(ps.getProperties()));
final int propsNum = props.length;
for (int j = 0; j < propsNum; ++j) {
propertiesAcc.add(props[j]);
}
} }
getAllChildPropertyHeadersRec(child, rows, propertiesAcc); getAllChildProperties(child, maxRows, propertiesAcc);
} }
} }
@ -305,13 +207,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
return true; return true;
} }
/**
* Thread note: Make sure to run this in the EDT as it causes GUI
* operations.
*
* @param selectedNode
*/
@Override @Override
@ThreadConfined(type = ThreadConfined.ThreadType.AWT)
public void setNode(Node selectedNode) { public void setNode(Node selectedNode) {
/* /*
@ -377,33 +274,26 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
* duplicates getDisplayName(). The current implementation does not * duplicates getDisplayName(). The current implementation does not
* allow the first property column to be moved. * allow the first property column to be moved.
*/ */
if (props.size() > 0) { if (props.isEmpty() == false) {
Node.Property<?> prop = props.remove(0); Node.Property<?> prop = props.remove(0);
((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(prop.getDisplayName()); ((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 assignColumns(props);
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();
}
outlineView.setPropertyColumns(propStrings); setColumnWidths();
// show the horizontal scroll panel and show all the content & header loadColumnSorting();
// 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);
if (root.getChildren().getNodesCount() != 0) { private void setColumnWidths() {
if (currentRoot.getChildren().getNodesCount() != 0) {
final Graphics graphics = outlineView.getGraphics(); final Graphics graphics = outlineView.getGraphics();
if (graphics != null) { if (graphics != null) {
final FontMetrics metrics = graphics.getFontMetrics(); final FontMetrics metrics = graphics.getFontMetrics();
@ -437,58 +327,31 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
// if there's no content just auto resize all columns // if there's no content just auto resize all columns
outline.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); outline.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
} }
}
loadSort(); private void assignColumns(List<Property<?>> props) {
// Get the columns setup with respect to names and sortability
/** String[] propStrings = new String[props.size() * 2];
* This custom renderer extends the renderer that was already being used for (int i = 0; i < props.size(); i++) {
* by the outline table. This renderer colors a row if the tags property final Property<?> prop = props.get(i);
* of the node is not empty. prop.setValue("ComparableColumnTTV", Boolean.TRUE); //NON-NLS
*/ //First property column is sorted initially
class ColorTagCustomRenderer extends DefaultOutlineCellRenderer { if (i == 0) {
prop.setValue("TreeColumnTTV", Boolean.TRUE); // Identifies special property representing first (tree) column. NON-NLS
private static final long serialVersionUID = 1L; prop.setValue("SortingColumnTTV", Boolean.TRUE); // TreeTableView should be initially sorted by this property column. NON-NLS
@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;
} }
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()) { if (currentRoot == null || propertiesMap.isEmpty()) {
return; return;
} }
@ -517,24 +380,13 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
} }
} }
static private final class ColumnSortInfo { /**
* Reads and applies the column sorting information persisted to the
private final int modelIndex; * preferences file. Must be called after loadColumnOrder() since it depends
private final int rank; * on propertiesMap being initialized, and before assignColumns since it
private final boolean order; * cannot set the sort on columns that have not been added to the table.
*/
private ColumnSortInfo(int modelIndex, int rank, boolean order) { private synchronized void loadColumnSorting() {
this.modelIndex = modelIndex;
this.rank = rank;
this.order = order;
}
private int getRank() {
return rank;
}
}
private synchronized void loadSort() {
if (currentRoot == null || propertiesMap.isEmpty()) { if (currentRoot == null || propertiesMap.isEmpty()) {
return; return;
} }
@ -543,48 +395,45 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
final TableFilterNode tfn = (TableFilterNode) currentRoot; final TableFilterNode tfn = (TableFilterNode) currentRoot;
final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class);
//organize property sorting information, sorted by rank //organize property sorting information, sorted by rank
TreeSet<ColumnSortInfo> treeSet = new TreeSet<>(Comparator.comparing(ColumnSortInfo::getRank)); TreeSet<ColumnSortInfo> treeSet = new TreeSet<>(Comparator.comparing(ColumnSortInfo::getRank));
propertiesMap.entrySet().stream() propertiesMap.entrySet().stream().forEach(entry -> {
.forEach(entry -> { final String propName = entry.getValue().getName();
final String propName = entry.getValue().getName(); //if the sort rank is undefined, it will be defaulted to 0 => unsorted.
//if the sort rank is undefined, it will be defaulted to 0 => unsorted. Integer sortRank = Integer.valueOf(preferences.get(ResultViewerPersistence.getColumnSortRankKey(tfn, propName), "0"));
Integer sortRank = Integer.valueOf(preferences.get(ResultViewerPersistence.getColumnSortRankKey(tfn, propName), "0")); //default to true => ascending
//default to true => ascending Boolean sortOrder = Boolean.valueOf(preferences.get(ResultViewerPersistence.getColumnSortOrderKey(tfn, propName), "true"));
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. //apply sort information in rank order.
treeSet.forEach(sortInfo -> outline.setColumnSorted(sortInfo.modelIndex, sortInfo.order, sortInfo.rank)); 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<Node.Property<?>> of the preferences in order * @return a List<Node.Property<?>> of the properties in the persisted
* order.
*/ */
private synchronized List<Node.Property<?>> loadColumnOrder() { private synchronized List<Node.Property<?>> 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<Property<?>> propertiesAcc = new LinkedHashSet<>(); Set<Property<?>> propertiesAcc = new LinkedHashSet<>();
this.getAllChildPropertyHeadersRec(currentRoot, 100, propertiesAcc); getAllChildProperties(currentRoot, 100, propertiesAcc);
List<Property<?>> props = new ArrayList<>(propertiesAcc);
List<Node.Property<?>> props = new ArrayList<>(propertiesAcc);
// If node is not table filter node, use default order for columns // If node is not table filter node, use default order for columns
TableFilterNode tfn; if (!(currentRoot instanceof TableFilterNode)) {
if (currentRoot instanceof TableFilterNode) {
tfn = (TableFilterNode) currentRoot;
} else {
// The node is not a TableFilterNode, columns are going to be in default order
return props; return props;
} }
final TableFilterNode tfn = ((TableFilterNode) currentRoot);
propertiesMap.clear(); propertiesMap.clear();
/* /*
* We load column index values into the properties map. If a property's * 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 * 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 // If none of the properties had previous settings, we should decrement
// each value by the number of properties to make the values 0-indexed. // each value by the number of properties to make the values 0-indexed.
if (noPreviousSettings) { if (noPreviousSettings) {
Integer[] keys = propertiesMap.keySet().toArray(new Integer[propertiesMap.keySet().size()]); ArrayList<Integer> keys = new ArrayList<>(propertiesMap.keySet());
for (int key : keys) { for (int key : keys) {
propertiesMap.put(key - props.size(), propertiesMap.get(key)); propertiesMap.put(key - props.size(), propertiesMap.remove(key));
propertiesMap.remove(key);
} }
} }
return new ArrayList<>(propertiesMap.values()); return new ArrayList<>(propertiesMap.values());
} }
@Override @Override
@NbBundle.Messages("DataResultViewerTable.title=Table")
public String getTitle() { public String getTitle() {
return NbBundle.getMessage(this.getClass(), "DataResultViewerTable.title"); return Bundle.DataResultViewerTable_title();
} }
@Override @Override
@ -637,6 +485,132 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
this.outlineView = null; this.outlineView = null;
super.clearComponent(); 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<Integer> 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<Integer> 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 { private class PleasewaitNodeListener extends NodeAdapter {
@ -652,23 +626,61 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
Node[] delta = nme.getDelta(); Node[] delta = nme.getDelta();
if (load && containsReal(delta)) { if (load && containsReal(delta)) {
load = false; load = false;
//JMTODO: this looks suspicious
if (SwingUtilities.isEventDispatchThread()) { if (SwingUtilities.isEventDispatchThread()) {
setupTable(nme.getNode()); setupTable(nme.getNode());
} else { } else {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> setupTable(nme.getNode()));
setupTable(nme.getNode());
});
} }
} }
} }
private boolean containsReal(Node[] delta) { private boolean containsReal(Node[] delta) {
for (Node n : delta) { return Stream.of(delta)
if (!n.getDisplayName().equals(PLEASEWAIT_NODE_DISPLAY_NAME)) { .map(Node::getDisplayName)
return true; .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;
} }
} }
} }