Merge remote-tracking branch 'origin/GP-1615_ghidravore_allowing_some_nodes_to_resist_filtering'

This commit is contained in:
ghidra1 2021-12-29 10:21:01 -05:00
commit cc6293a10c
15 changed files with 387 additions and 227 deletions

View file

@ -129,6 +129,7 @@ public class CreateTypeDefAction extends AbstractTypeDefAction {
GTreeNode finalParentNode = info.getParentNode();
String newNodeName = newTypeDef.getName();
dataTypeManager.flushEvents();
gTree.startEditing(finalParentNode, newNodeName);
}

View file

@ -23,7 +23,6 @@ import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.tree.TreePath;
import org.junit.*;
@ -291,16 +290,12 @@ public class DataTypeCopyMoveDragTest extends AbstractGhidraHeadedIntegrationTes
CategoryNode miscNode = (CategoryNode) programNode.getChild("MISC");
expandNode(miscNode);
TreePath structureNodePath = structureNode.getTreePath();
TreePath category3Path = category3Node.getTreePath();
TreePath miscPath = miscNode.getTreePath();
// copy/drag ArrayStruct to MISC
copyNodeToNode(structureNode, miscNode);
structureNode = (DataTypeNode) tree.getViewNodeForPath(structureNodePath);
category3Node = (CategoryNode) tree.getViewNodeForPath(category3Path);
miscNode = (CategoryNode) tree.getViewNodeForPath(miscPath);
structureNode = (DataTypeNode) tree.getViewNode(structureNode);
category3Node = (CategoryNode) tree.getViewNode(category3Node);
miscNode = (CategoryNode) tree.getViewNode(miscNode);
CategoryNode parent = miscNode;
DataTypeNode node =
@ -324,16 +319,12 @@ public class DataTypeCopyMoveDragTest extends AbstractGhidraHeadedIntegrationTes
CategoryNode miscNode = (CategoryNode) programNode.getChild("MISC");
expandNode(miscNode);
TreePath structureNodePath = structureNode.getTreePath();
TreePath category3Path = category3Node.getTreePath();
TreePath miscPath = miscNode.getTreePath();
// copy/drag ArrayStruct to MISC
copyNodeToNode(structureNode, miscNode);
structureNode = (DataTypeNode) tree.getViewNodeForPath(structureNodePath);
category3Node = (CategoryNode) tree.getViewNodeForPath(category3Path);
miscNode = (CategoryNode) tree.getViewNodeForPath(miscPath);
structureNode = (DataTypeNode) tree.getViewNode(structureNode);
category3Node = (CategoryNode) tree.getViewNode(category3Node);
miscNode = (CategoryNode) tree.getViewNode(miscNode);
DataTypeNode node = (DataTypeNode) miscNode.getChild(structName);
assertNotNull(node);
@ -509,23 +500,19 @@ public class DataTypeCopyMoveDragTest extends AbstractGhidraHeadedIntegrationTes
CategoryNode miscNode = (CategoryNode) programNode.getChild("MISC");
expandNode(miscNode);
TreePath structureNodePath = structureNode.getTreePath();
TreePath category3Path = category3Node.getTreePath();
TreePath miscPath = miscNode.getTreePath();
// drag/move ArrayStruct to MISC/ArrayStruct
DataTypeNode miscStructureNode = (DataTypeNode) miscNode.getChild("ArrayStruct");
dragNodeToNode(structureNode, miscStructureNode);
pressButtonOnOptionDialog("No");
structureNode = (DataTypeNode) tree.getViewNodeForPath(structureNodePath);
structureNode = (DataTypeNode) tree.getViewNode(structureNode);
assertNotNull(structureNode.getParent());
category3Node = (CategoryNode) tree.getViewNodeForPath(category3Path);
category3Node = (CategoryNode) tree.getViewNode(category3Node);
assertNotNull(category3Node.getChild("ArrayStruct"));
miscNode = (CategoryNode) tree.getViewNodeForPath(miscPath);
miscNode = (CategoryNode) tree.getViewNode(miscNode);
DataTypeNode node = (DataTypeNode) miscNode.getChild("ArrayStruct");
assertTrue(!structure.isEquivalent(node.getDataType()));
}
@ -583,21 +570,17 @@ public class DataTypeCopyMoveDragTest extends AbstractGhidraHeadedIntegrationTes
DataTypeNode miscStructureNode = (DataTypeNode) miscNode.getChild("ArrayStruct");
DataType origDt = miscStructureNode.getDataType();
TreePath structureNodePath = structureNode.getTreePath();
TreePath category3Path = category3Node.getTreePath();
TreePath miscPath = miscNode.getTreePath();
copyNodeToNode(structureNode, miscStructureNode);
pressButtonOnOptionDialog("No");
structureNode = (DataTypeNode) tree.getViewNodeForPath(structureNodePath);
structureNode = (DataTypeNode) tree.getViewNode(structureNode);
assertNotNull(structureNode.getParent());
category3Node = (CategoryNode) tree.getViewNodeForPath(category3Path);
category3Node = (CategoryNode) tree.getViewNode(category3Node);
assertNotNull(category3Node.getChild("ArrayStruct"));
miscNode = (CategoryNode) tree.getViewNodeForPath(miscPath);
miscNode = (CategoryNode) tree.getViewNode(miscNode);
DataTypeNode node = (DataTypeNode) miscNode.getChild(structName);
assertEquals(origDt, node.getDataType());
}
@ -621,21 +604,17 @@ public class DataTypeCopyMoveDragTest extends AbstractGhidraHeadedIntegrationTes
DataTypeNode miscStructureNode = (DataTypeNode) miscNode.getChild("ArrayStruct");
DataType miscStructure = miscStructureNode.getDataType();
TreePath structureNodePath = structureNode.getTreePath();
TreePath category3Path = category3Node.getTreePath();
TreePath miscPath = miscNode.getTreePath();
copyNodeToNode(structureNode, miscStructureNode);
pressButtonOnOptionDialog("Yes");
structureNode = (DataTypeNode) tree.getViewNodeForPath(structureNodePath);
structureNode = (DataTypeNode) tree.getViewNode(structureNode);
assertNotNull(structureNode.getParent());
category3Node = (CategoryNode) tree.getViewNodeForPath(category3Path);
category3Node = (CategoryNode) tree.getViewNode(category3Node);
assertNotNull(category3Node.getChild(structName));
miscNode = (CategoryNode) tree.getViewNodeForPath(miscPath);
miscNode = (CategoryNode) tree.getViewNode(miscNode);
DataTypeNode node = (DataTypeNode) miscNode.getChild(structName);
assertTrue(structure.isEquivalent(node.getDataType()));

View file

@ -25,7 +25,8 @@ import java.awt.event.MouseListener;
import java.io.PrintWriter;
import java.util.*;
import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.swing.*;
import javax.swing.Timer;
@ -60,7 +61,7 @@ public class GTree extends JPanel implements BusyListener {
/**
* This is the root node of the tree's data model. It may or may not be the root node
* that is currently being displayed by the tree. If there is currently a
* that is currently being displayed by the tree. If there is currently a
* filter applied, then then the displayed root node will be a clone whose children have been
* trimmed to only those that match the filter. By keeping this variable around, we can give
* this node to clients, regardless of the root node visible in the tree.
@ -68,16 +69,16 @@ public class GTree extends JPanel implements BusyListener {
private volatile GTreeNode realModelRootNode;
/**
* This is the root that is currently being displayed. This node will be either exactly the
* This is the root that is currently being displayed. This node will be either exactly the
* same instance as the realModelRootNode (if no filter has been applied) or it will be the
* filtered clone of the realModelRootNode.
* filtered clone of the realModelRootNode.
*/
private volatile GTreeNode realViewRootNode;
/**
* The rootParent is a node that is assigned as the parent to the realRootNode. It's primary purpose is
* to allow nodes access to the tree. It overrides the getTree() method on GTreeNode to return
* this tree. This eliminated the need for clients to create special root nodes that had
* this tree. This eliminated the need for clients to create special root nodes that had
* public setTree/getTree methods.
*/
private GTreeRootParentNode rootParent = new GTreeRootParentNode(this);
@ -107,6 +108,7 @@ public class GTree extends JPanel implements BusyListener {
private GTreeFilter filter;
private GTreeFilterProvider filterProvider;
private SwingUpdateManager filterUpdateManager;
private Set<GTreeNode> ignoredNodes = new HashSet<>();
/**
* Creates a GTree with the given root node. The created GTree will use a threaded model
@ -241,6 +243,7 @@ public class GTree extends JPanel implements BusyListener {
}
public void dispose() {
ignoredNodes.clear();
filterUpdateManager.dispose();
worker.dispose();
@ -269,11 +272,29 @@ public class GTree extends JPanel implements BusyListener {
}
public void filterChanged() {
ignoredNodes.clear();
updateModelFilter();
}
private void ignoreFilter(GTreeNode node) {
if (!isFiltered()) {
return;
}
ignoredNodes.add(node);
updateModelFilter();
}
protected void updateModelFilter() {
filter = filterProvider.getFilter();
if (filter != null && !ignoredNodes.isEmpty()) {
filter = new IgnoredNodesGtreeFilter(filter, ignoredNodes);
}
// Normally this call is made from the update manager, which means we do not need to stop
// it manually. However, this method may be called directly by the tree. In that case,
// we will be performing a filter now, so there is no need to allow the update manager
// to keep buffering.
filterUpdateManager.stop();
if (lastFilterTask != null) {
// it is safe to repeatedly call cancel
@ -324,7 +345,7 @@ public class GTree extends JPanel implements BusyListener {
}
/**
* Signal to the tree that it should record its expanded and selected state when a
* Signal to the tree that it should record its expanded and selected state when a
* new filter is applied
*/
void saveFilterRestoreState() {
@ -556,24 +577,50 @@ public class GTree extends JPanel implements BusyListener {
return null;
}
/**
* Gets the model node for the given node. This is useful if the node that is in the path has
* been replaced by a new node that is equal, but a different instance. One way this happens
* is if the tree is filtered and therefor the displayed nodes are clones of the model nodes.
* This can also happen if the tree nodes are rebuilt for some reason.
*
* @param node the node
* @return the corresponding model node in the tree. If the tree is filtered the viewed node
* will be a clone of the corresponding model node.
*/
public GTreeNode getModelNode(GTreeNode node) {
return getNodeForPath(getModelRoot(), node.getTreePath());
}
/**
* Gets the model node for the given path. This is useful if the node that is in the path has
* been replaced by a new node that is equal, but a different instance. One way this happens
* is if the tree is filtered and therefor the displayed nodes are clones of the model nodes. This
* can also happen if the tree nodes are rebuilt for some reason.
* is if the tree is filtered and therefor the displayed nodes are clones of the model nodes.
* This can also happen if the tree nodes are rebuilt for some reason.
*
* @param path the path of the node
* @return the corresponding model node in the tree. If the tree is filtered the viewed node will
* be a clone of the corresponding model node.
* @return the corresponding model node in the tree. If the tree is filtered the viewed node
* will be a clone of the corresponding model node.
*/
public GTreeNode getModelNodeForPath(TreePath path) {
return getNodeForPath(getModelRoot(), path);
}
/**
* Gets the view node for the given path. This is useful to translate to a tree path that
* is valid for the currently displayed tree. (Remember that if the tree is filtered,
* then the displayed nodes are clones of the model nodes.)
* Gets the view node for the given node. This is useful to translate to a tree path that is
* valid for the currently displayed tree. (Remember that if the tree is filtered, then the
* displayed nodes are clones of the model nodes.)
*
* @param node the node
* @return the current node in the displayed (possibly filtered) tree
*/
public GTreeNode getViewNode(GTreeNode node) {
return getNodeForPath(getViewRoot(), node.getTreePath());
}
/**
* Gets the view node for the given path. This is useful to translate to a tree path that is
* valid for the currently displayed tree. (Remember that if the tree is filtered, then the
* displayed nodes are clones of the model nodes.)
*
* @param path the path of the node
* @return the current node in the displayed (possibly filtered) tree
@ -594,8 +641,9 @@ public class GTree extends JPanel implements BusyListener {
}
return null; // invalid path--the root of the path is not equal to our root!
}
if (node.getRoot() == root) {
return node;
return node; // this node is a valid child of the given root
}
GTreeNode parentNode = getNodeForPath(root, path.getParentPath());
@ -721,7 +769,7 @@ public class GTree extends JPanel implements BusyListener {
}
/**
* Sets the root node for this tree.
* Sets the root node for this tree.
* <P>
* NOTE: if this method is not called from the Swing thread, then the root node will be set
* later on the Swing thread. That is, this method will return before the work has been done.
@ -768,7 +816,7 @@ public class GTree extends JPanel implements BusyListener {
/**
* This method returns the root node that was provided to the tree by the client, whether from the
* constructor or from {@link #setRootNode(GTreeNode)}.
* constructor or from {@link #setRootNode(GTreeNode)}.
* This node represents the data model and always contains all the nodes regardless of any filter
* being applied. If a filter is applied to the tree, then this is not the actual root node being
* displayed by the {@link JTree}.
@ -782,7 +830,7 @@ public class GTree extends JPanel implements BusyListener {
* This method returns the root node currently being displayed by the {@link JTree}. If there
* are no filters applied, then this will be the same as the model root (See {@link #getModelRoot()}).
* If a filter is applied, then this will be a clone of the model root that contains clones of all
* nodes matching the filter.
* nodes matching the filter.
* @return the root node currently being display by the {@link JTree}
*/
public GTreeNode getViewRoot() {
@ -942,15 +990,96 @@ public class GTree extends JPanel implements BusyListener {
tree.setEditable(editable);
}
// Waits for the given model node, passing it to the consumer when available
private void getModelNode(GTreeNode parent, String childName, Consumer<GTreeNode> consumer) {
int expireMs = 3000;
Supplier<GTreeNode> supplier = () -> {
GTreeNode modelParent = getModelNode(parent);
if (modelParent != null) {
return modelParent.getChild(childName);
}
return null;
};
ExpiringSwingTimer.get(supplier, expireMs, consumer);
}
// Waits for the given view node, passing it to the consumer when available
private void getViewNode(GTreeNode parent, String childName, Consumer<GTreeNode> consumer) {
int expireMs = 3000;
Supplier<GTreeNode> supplier = () -> {
GTreeNode viewParent = getViewNode(parent);
if (viewParent != null) {
return viewParent.getChild(childName);
}
return null;
};
ExpiringSwingTimer.get(supplier, expireMs, consumer);
}
/**
* Requests that the node with the given name, in the given parent, be edited. This
* operation is asynchronous. This request will be buffered as needed to wait for
* the given node to be added to the parent, up to a timeout period.
* A specialized method that will get the child node from the given parent node when it
* becomes available to the model. This method will ensure that the named child passes any
* current filter in order for the child to appear in the tree. This effect is temporary and
* will be undone when next the filter changes.
*
* <p>This method is intended to be used by clients using an asynchronous node model, where
* new nodes will get created by application-level events. Such clients may wish to perform
* work when newly created nodes become available. This method simplifies the concurrent
* nature of the GTree, asynchronous nodes and the processing of asynchronous application-level
* events by providing a callback mechanism for clients. <b>This method is non-blocking.</b>
*
* <p>Note: this method assumes that the given parent node is in the view and not filtered
* out of the view. This method makes no attempt to ensure the given parent node passes any
* existing filter.
*
* <p>Note: this method will not wait forever for the given node to appear. It will eventually
* give up if the node never arrives.
*
* @param parent the model's parent node. If the view's parent node is passed, it will
* be translated to the model node.
* @param childName the name of the desired child
* @param consumer the consumer callback to which the child node will be given when available
*/
public void forceNewNodeIntoView(GTreeNode parent, String childName,
Consumer<GTreeNode> consumer) {
/*
If the GTree were to use Java's CompletableStage API, then the code below
could be written thusly:
tree.getNewNode(modelParent, newName)
.thenCompose(newModelChild -> {
tree.ignoreFilter(newModelChild);
return tree.getNewNode(viewParent, newName);
))
.thenAccept(consumer);
*/
// ensure we operate on the model node which will always have the given child not the view
// node, which may have its child filtered
GTreeNode modelParent = getModelNode(parent);
getModelNode(modelParent, childName, newModelChild -> {
// force the filter to accept the new node
ignoreFilter(newModelChild);
// Wait for the view to update from any filtering that may take place
getViewNode(modelParent, childName, consumer);
});
}
/**
* Requests that the node with the given name, in the given parent, be edited. This operation
* is asynchronous. This request will be buffered as needed to wait for the given node to be
* added to the parent, up to a timeout period.
*
* @param parent the parent node
* @param childName the name of the child to edit
*/
public void startEditing(GTreeNode parent, final String childName) {
public void startEditing(GTreeNode parent, String childName) {
// we call this here, even though the JTree will do this for us, so that we will trigger
// a load call before this task is run, in case lazy nodes are involved in this tree,
@ -959,31 +1088,36 @@ public class GTree extends JPanel implements BusyListener {
//
// The request to edit the node may be for a node that has not yet been added to this
// tree. Further, some clients will buffer events, which means that the node the client
// tree. Further, some clients will buffer events, which means that the node the client
// wishes to edit may not yet be in the parent node even if we run this request later on
// the Swing thread. To deal with this, we use a construct that will run our request
// once the given node has been added to the parent.
//
BooleanSupplier isReady = () -> parent.getChild(childName) != null;
int expireMs = 3000;
ExpiringSwingTimer.runWhen(isReady, expireMs, () -> {
runTask(new GTreeStartEditingTask(GTree.this, tree, parent, childName));
GTreeNode modelParent = getModelNode(parent);
forceNewNodeIntoView(modelParent, childName, viewNode -> {
runTask(new GTreeStartEditingTask(GTree.this, tree, viewNode));
});
}
/**
* Requests that the node be edited. This operation is asynchronous.
* Requests that the node be edited. This operation is asynchronous.
*
* @param child the node to edit
* @param node the node to edit
*/
public void startEditing(GTreeNode child) {
public void startEditing(GTreeNode node) {
// we call this here, even though the JTree will do this for us, so that we will trigger
// a load call before this task is run, in case lazy nodes are involved in this tree,
// which must be loaded before we can edit
expandPath(child.getParent());
expandPath(node.getParent());
runTask(new GTreeStartEditingTask(GTree.this, tree, child));
GTreeNode viewNode = getViewNode(node);
if (viewNode == null) {
startEditing(node.getParent(), node.getName());
return;
}
runTask(new GTreeStartEditingTask(GTree.this, tree, viewNode));
}
@Override
@ -1228,7 +1362,7 @@ public class GTree extends JPanel implements BusyListener {
private void updateDefaultKeyBindings() {
// Remove the edit keybinding, as the GTree triggers editing via a task, since it
// is multi-threaded. Doing this allows users to assign their own key bindings to
// is multi-threaded. Doing this allows users to assign their own key bindings to
// the edit task.
KeyBindingUtils.clearKeyBinding(this, "startEditing");
}

View file

@ -25,12 +25,11 @@ import ghidra.util.task.TaskMonitor;
public class GTreeFilterTask extends GTreeTask {
private final GTreeFilter filter;
private boolean cancelledProgramatically;
private volatile boolean cancelledProgramatically;
public GTreeFilterTask(GTree tree, GTreeFilter filter) {
super(tree);
this.filter = filter;
tree.saveFilterRestoreState();
}

View file

@ -18,7 +18,7 @@ package docking.widgets.tree;
import javax.swing.JTree;
import javax.swing.tree.TreePath;
import ghidra.util.SystemUtilities;
import ghidra.util.Swing;
import ghidra.util.task.TaskMonitor;
import ghidra.util.worker.PriorityJob;
@ -42,7 +42,7 @@ public abstract class GTreeTask extends PriorityJob {
if (isCancelled()) {
return;
}
SystemUtilities.runSwingNow(new CheckCancelledRunnable(runnable));
Swing.runNow(new CheckCancelledRunnable(runnable));
}
/**
@ -61,7 +61,7 @@ public abstract class GTreeTask extends PriorityJob {
// note: call this on the Swing thread, since the Swing thread maintains the node state
// (we have seen errors where the tree will return nodes that are in the process
// of being disposed)
GTreeNode nodeForPath = SystemUtilities.runSwingNow(() -> tree.getViewNodeForPath(path));
GTreeNode nodeForPath = Swing.runNow(() -> tree.getViewNodeForPath(path));
if (nodeForPath != null) {
return nodeForPath.getTreePath();
}
@ -70,7 +70,7 @@ public abstract class GTreeTask extends PriorityJob {
//==================================================================================================
// Inner Classes
//==================================================================================================
//==================================================================================================
class CheckCancelledRunnable implements Runnable {
private final Runnable runnable;

View file

@ -30,7 +30,7 @@ import ghidra.util.SystemUtilities;
public class GTreeModel implements TreeModel {
private volatile GTreeNode root;
private List<TreeModelListener> listeners = new ArrayList<TreeModelListener>();
private List<TreeModelListener> listeners = new ArrayList<>();
private boolean isFiringNodeStructureChanged;
private volatile boolean eventsEnabled = true;
@ -82,9 +82,9 @@ public class GTreeModel implements TreeModel {
// This can happen if the client code mutates the children of this node in a background
// thread such that there are fewer child nodes on this node, and then before the tree
// is notified, the JTree attempts to access a child that is no longer present. The
// GTree design specifically allows this situation to occur as a trade off for
// GTree design specifically allows this situation to occur as a trade off for
// better performance when performing bulk operations (such as filtering). If this
// does occur, this can be handled easily by temporarily returning a dummy node and
// does occur, this can be handled easily by temporarily returning a dummy node and
// scheduling a node structure changed event to reset the JTree.
Swing.runLater(() -> fireNodeStructureChanged((GTreeNode) parent));
return new InProgressGTreeNode();
@ -125,17 +125,17 @@ public class GTreeModel implements TreeModel {
"GTreeModel.fireNodeStructuredChanged() must be " + "called from the AWT thread");
// If the tree is filtered and this is called on the original node, we have to
// translate the node to a view node (one the jtree knows).
// translate the node to a view node (one the jtree knows).
GTreeNode viewNode = convertToViewNode(changedNode);
if (viewNode == null) {
return;
}
if (viewNode != changedNode) {
// This means we are filtered and since the original node's children are invalid,
// This means we are filtered and since the original node's children are invalid,
// then the filtered children are invalid also. So clear out the children by
// setting an empty list as we don't want to trigger the node to regenerate its
// children which happens if you set the children to null.
// setting an empty list as we don't want to trigger the node to regenerate its
// children which happens if you set the children to null.
//
// This won't cause a second event to the jtree because we are protected
// by the isFiringNodeStructureChanged variable
@ -170,9 +170,9 @@ public class GTreeModel implements TreeModel {
if (viewNode == null) {
return;
}
// Note - we are passing in the treepath of the node that changed. The javadocs in
// Note - we are passing in the treepath of the node that changed. The javadocs in
// TreemodelListener seems to imply that you need to pass in the treepath of the parent
// of the node that changed and then the indexes of the children that changed. But this
// of the node that changed and then the indexes of the children that changed. But this
// works and is cheaper then computing the index of the node that changed.
TreeModelEvent event = new TreeModelEvent(this, viewNode.getTreePath());
@ -247,7 +247,7 @@ public class GTreeModel implements TreeModel {
}
GTree tree = root.getTree();
if (tree != null) {
return tree.getViewNodeForPath(node.getTreePath());
return tree.getViewNode(node);
}
return null;
}

View file

@ -0,0 +1,49 @@
/* ###
* IP: GHIDRA
*
* 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 docking.widgets.tree.support;
import java.util.Objects;
import java.util.Set;
import docking.widgets.tree.GTreeNode;
/**
* GTreeFilter that allows for some nodes that are never filtered out.
*/
public class IgnoredNodesGtreeFilter implements GTreeFilter {
private GTreeFilter filter;
private Set<GTreeNode> ignoredNodes;
public IgnoredNodesGtreeFilter(GTreeFilter filter, Set<GTreeNode> ignoredNodes) {
this.filter = Objects.requireNonNull(filter);
this.ignoredNodes = Objects.requireNonNull(ignoredNodes);
}
@Override
public boolean acceptsNode(GTreeNode node) {
if (ignoredNodes.contains(node)) {
return true;
}
return filter.acceptsNode(node);
}
@Override
public boolean showFilterMatches() {
return filter.showFilterMatches();
}
}

View file

@ -98,7 +98,7 @@ public class GTreeSelectPathsTask extends GTreeTask {
selectionModel.setSelectionPaths(treePaths, origin);
if (treePaths != null && treePaths.length > 0) {
// Scroll to the last item, as the tree will make the given path appear at the
// Scroll to the last item, as the tree will make the given path appear at the
// bottom of the view. By scrolling the last item, all the selected items above
// this one will appear in the view as well.
jTree.scrollPathToVisible(treePaths[treePaths.length - 1]);

View file

@ -15,8 +15,7 @@
*/
package docking.widgets.tree.tasks;
import java.util.HashSet;
import java.util.Set;
import java.util.Objects;
import javax.swing.CellEditor;
import javax.swing.JTree;
@ -25,29 +24,17 @@ import javax.swing.event.ChangeEvent;
import javax.swing.tree.TreePath;
import docking.widgets.tree.*;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import util.CollectionUtils;
public class GTreeStartEditingTask extends GTreeTask {
private final GTreeNode parent;
private final String childName;
private GTreeNode editNode;
public GTreeStartEditingTask(GTree gTree, JTree jTree, GTreeNode parent, String childName) {
super(gTree);
this.parent = parent;
this.childName = childName;
}
private final GTreeNode modelParent;
private final GTreeNode editNode;
public GTreeStartEditingTask(GTree gTree, JTree jTree, GTreeNode editNode) {
super(gTree);
this.parent = editNode.getParent();
this.childName = editNode.getName();
this.modelParent = tree.getModelNode(editNode.getParent());
this.editNode = editNode;
}
@ -67,83 +54,29 @@ public class GTreeStartEditingTask extends GTreeTask {
}
private void edit() {
if (editNode == null) {
editNode = parent.getChild(childName);
if (editNode == null) {
Msg.debug(this, "Can't find node \"" + childName + "\" to edit.");
return;
}
}
TreePath path = editNode.getTreePath();
final Set<GTreeNode> childrenBeforeEdit = new HashSet<>(parent.getChildren());
final CellEditor cellEditor = tree.getCellEditor();
CellEditor cellEditor = tree.getCellEditor();
cellEditor.addCellEditorListener(new CellEditorListener() {
@Override
public void editingCanceled(ChangeEvent e) {
cellEditor.removeCellEditorListener(this);
SystemUtilities.runSwingLater(this::reselectNode);
reselectNode();
}
@Override
public void editingStopped(ChangeEvent e) {
String newName = Objects.toString(cellEditor.getCellEditorValue());
cellEditor.removeCellEditorListener(this);
SystemUtilities.runSwingLater(this::reselectNodeHandlingPotentialChildChange);
tree.forceNewNodeIntoView(modelParent, newName, newViewChild -> {
tree.setSelectedNode(newViewChild);
});
}
/**
* Unusual Code Alert!: This method handles the case where editing of a node triggers
* a new node to be created. In this case, reselecting the
* node that was edited can leave the tree with a selection that
* points to a removed node, which has bad consequences to clients.
* We work around this issue by retrieving the node after the edit
* has finished and been applied.
*/
private void reselectNode() {
String newName = editNode.getName();
GTreeNode newChild = parent.getChild(newName);
if (newChild == null) {
throw new AssertException("Unable to find new node by name: " + newName);
}
tree.setSelectedNode(newChild);
}
/**
* Unusual Code Alert!: This method handles the case where editing of a node triggers
* a new node to be created. In this case, reselecting the
* node that was edited can leave the tree with a selection that
* points to a removed node, which has bad consequences to clients.
* We work around this issue by retrieving the node after the edit
* has finished and been applied.
*
* This method takes into account the fact that we are not given
* the new name of the node in our editingStopped() callback.
* As such, we have to deduce the newly added node, based upon
* the state of the edited node's parent, both before and after
* the edit.
*/
private void reselectNodeHandlingPotentialChildChange() {
SystemUtilities.runSwingLater(this::doReselectNodeHandlingPotentialChildChange);
}
private void doReselectNodeHandlingPotentialChildChange() {
Set<GTreeNode> childrenAfterEdit = new HashSet<>(parent.getChildren());
if (childrenAfterEdit.equals(childrenBeforeEdit)) {
reselectNode(); // default re-select--the original child is still there
return;
}
// we have to figure out the new node to select
childrenAfterEdit.removeAll(childrenBeforeEdit);
if (childrenAfterEdit.size() != 1) {
return; // no way for us to figure out the correct child to edit
}
GTreeNode newChild = CollectionUtils.any(childrenAfterEdit);
tree.setSelectedNode(newChild);
String name = editNode.getName();
GTreeNode newModelChild = modelParent.getChild(name);
tree.setSelectedNode(newModelChild);
}
});

View file

@ -17,13 +17,15 @@ package generic.timer;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import java.util.function.*;
import utility.function.Dummy;
/**
* This class allows clients to run swing action at some point in the future, when the given
* This class allows clients to run swing action at some point in the future, when the given
* condition is met, allowing for the task to timeout. While this class implements the
* {@link GhidraTimer} interface, it is really meant to be used to execute a code snippet one
* time at some point in the future.
* time at some point in the future.
*
* <p>Both the call to check for readiness and the actual client code will be run on the Swing
* thread.
@ -38,10 +40,10 @@ public class ExpiringSwingTimer extends GhidraSwingTimer {
private AtomicBoolean didRun = new AtomicBoolean();
/**
* Runs the given client runnable when the given condition returns true. The returned timer
* Runs the given client runnable when the given condition returns true. The returned timer
* will be running.
*
* <p>Once the timer has performed the work, any calls to start the returned timer will
* <p>Once the timer has performed the work, any calls to start the returned timer will
* not perform any work. You can check {@link #didRun()} to see if the work has been completed.
*
* @param isReady true if the code should be run
@ -49,15 +51,47 @@ public class ExpiringSwingTimer extends GhidraSwingTimer {
* @param runnable the code to run
* @return the timer object that is running, which will execute the given code when ready
*/
public static ExpiringSwingTimer runWhen(BooleanSupplier isReady,
int expireMs,
public static ExpiringSwingTimer runWhen(BooleanSupplier isReady, int expireMs,
Runnable runnable) {
// Note: we could let the client specify the period, but that would add an extra argument
// Note: we could let the client specify the delay, but that would add an extra argument
// to this method. For now, just use something reasonable.
int delay = 250;
ExpiringSwingTimer timer =
new ExpiringSwingTimer(delay, expireMs, isReady, runnable);
ExpiringSwingTimer timer = new ExpiringSwingTimer(delay, expireMs, isReady, runnable);
timer.start();
return timer;
}
/**
* Calls the given consumer with the non-null value returned from the given supplier. The
* returned timer will be running.
*
* <p>Once the timer has performed the work, any calls to start the returned timer will
* not perform any work. You can check {@link #didRun()} to see if the work has been completed.
*
* @param <T> the type used by the supplier and consumer
* @param supplier the supplier of the desired value
* @param expireMs the amount of time past which the code will not be run
* @param consumer the consumer to be called with the supplier's value
* @return the timer object that is running, which will execute the given code when ready
*/
public static <T> ExpiringSwingTimer get(Supplier<T> supplier, int expireMs,
Consumer<T> consumer) {
// Note: we could let the client specify the delay, but that would add an extra argument
// to this method. For now, just use something reasonable.
int delay = 250;
BooleanSupplier isReady = () -> {
T t = supplier.get();
if (t == null) {
return false;
}
consumer.accept(t);
return true;
};
Runnable dummy = Dummy.runnable();
ExpiringSwingTimer timer = new ExpiringSwingTimer(delay, expireMs, isReady, dummy);
timer.start();
return timer;
}
@ -65,7 +99,7 @@ public class ExpiringSwingTimer extends GhidraSwingTimer {
/**
* Constructor
*
* <p>Note: this class sets the parent's initial delay to 0. This is to allow the client
* <p>Note: this class sets the parent's initial delay to 0. This is to allow the client
* code to be executed without delay when the ready condition is true.
*
* @param delay the delay between calls to check <code>isReady</code>
@ -73,8 +107,7 @@ public class ExpiringSwingTimer extends GhidraSwingTimer {
* @param expireMs the amount of time past which the code will not be run
* @param runnable the code to run
*/
public ExpiringSwingTimer(int delay, int expireMs, BooleanSupplier isReady,
Runnable runnable) {
public ExpiringSwingTimer(int delay, int expireMs, BooleanSupplier isReady, Runnable runnable) {
super(0, delay, null);
this.expireMs = expireMs;
this.isReady = isReady;
@ -84,7 +117,7 @@ public class ExpiringSwingTimer extends GhidraSwingTimer {
}
/**
* Returns true if the client runnable was run
* Returns true if the client runnable was run
* @return true if the client runnable was run
*/
public boolean didRun() {

View file

@ -17,9 +17,8 @@ package generic.timer;
import static org.junit.Assert.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import org.junit.Test;
@ -47,6 +46,33 @@ public class ExpiringSwingTimerTest extends AbstractGenericTest {
assertEquals("Client code was run more than once", 1, runCount.get());
}
@Test
public void testGet() {
String expectedResult = "Test result";
int waitCount = 2;
AtomicInteger counter = new AtomicInteger();
Supplier<String> supplier = () -> {
if (counter.incrementAndGet() > waitCount) {
return expectedResult;
}
return null;
};
AtomicReference<String> result = new AtomicReference<>();
AtomicInteger runCount = new AtomicInteger();
Consumer<String> consumer = s -> {
runCount.incrementAndGet();
result.set(s);
};
ExpiringSwingTimer.get(supplier, 10000, consumer);
waitFor(() -> runCount.get() > 0);
assertEquals("", expectedResult, result.get());
assertTrue("Timer did not wait for the condition to be true", counter.get() > waitCount);
assertEquals("Client code was run more than once", 1, runCount.get());
}
@Test
public void testRunWhenReady_Timeout() {
@ -63,6 +89,22 @@ public class ExpiringSwingTimerTest extends AbstractGenericTest {
assertFalse(didRun.get());
}
@Test
public void testGet_Timeout() {
Supplier<String> supplier = () -> {
return null;
};
AtomicBoolean didRun = new AtomicBoolean();
Consumer<String> consumer = s -> didRun.set(true);
ExpiringSwingTimer timer = ExpiringSwingTimer.get(supplier, 500, consumer);
waitFor(() -> !timer.isRunning());
assertFalse(didRun.get());
}
@Test
public void testWorkOnlyHappensOnce() {

View file

@ -19,12 +19,13 @@ import java.util.List;
import javax.swing.tree.TreePath;
import docking.widgets.tree.GTreeNode;
import ghidra.framework.main.datatree.*;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
/**
* Common methods appropriate for both the {@link FrontEndProjectTreeContext} and the
* Common methods appropriate for both the {@link FrontEndProjectTreeContext} and the
* {@link DialogProjectTreeContext}. The project tree actions require that the contexts be
* separate even though they need many of the same methods. By extracting the methods to this
* interface, the contexts can be kept separate, but can share action code.
@ -67,4 +68,9 @@ public interface ProjectTreeContext {
*/
public TreePath[] getSelectionPaths();
/**
* Returns the node that represents the context object for this context
* @return the node
*/
public GTreeNode getContextNode();
}

View file

@ -21,6 +21,7 @@ import java.util.List;
import javax.swing.tree.TreePath;
import docking.ActionContext;
import docking.widgets.tree.GTreeNode;
import ghidra.framework.main.datatable.ProjectTreeContext;
import ghidra.framework.model.*;
@ -93,4 +94,8 @@ public class DialogProjectTreeContext extends ActionContext implements ProjectTr
return selectedFiles.size();
}
@Override
public GTreeNode getContextNode() {
return (GTreeNode) super.getContextObject();
}
}

View file

@ -20,6 +20,7 @@ import java.util.List;
import javax.swing.tree.TreePath;
import docking.ComponentProvider;
import docking.widgets.tree.GTreeNode;
import ghidra.framework.main.datatable.ProjectDataContext;
import ghidra.framework.main.datatable.ProjectTreeContext;
import ghidra.framework.model.*;
@ -56,4 +57,9 @@ public class FrontEndProjectTreeContext extends ProjectDataContext
public DataTree getTree() {
return tree;
}
@Override
public GTreeNode getContextNode() {
return (GTreeNode) super.getContextObject();
}
}

View file

@ -16,8 +16,6 @@
package ghidra.framework.main.projectdata.actions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
@ -26,14 +24,15 @@ import docking.action.MenuData;
import docking.widgets.tree.GTreeNode;
import ghidra.framework.main.datatable.ProjectTreeContext;
import ghidra.framework.main.datatree.DataTree;
import ghidra.framework.main.datatree.DomainFileNode;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.util.InvalidNameException;
import ghidra.util.Swing;
import ghidra.util.exception.AssertException;
import resources.ResourceManager;
public class ProjectDataNewFolderAction<T extends ProjectTreeContext> extends ContextSpecificAction<T> {
public class ProjectDataNewFolderAction<T extends ProjectTreeContext>
extends ContextSpecificAction<T> {
private static Icon icon = ResourceManager.loadImage("images/folder_add.png");
@ -53,24 +52,14 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext> extends Co
return (context.getFolderCount() + context.getFileCount()) == 1;
}
/**
* Create a new folder for the selected node that represents
* a folder.
*/
private void createNewFolder(T context) {
DomainFolder parentFolder = getFolder(context);
DomainFolder newFolder = createNewFolderWithDefaultName(parentFolder);
GTreeNode parent = getParentNode(context);
DataTree tree = context.getTree();
Swing.runLater(() -> {
GTreeNode node = findNodeForFolder(tree, newFolder);
if (node != null) {
tree.setEditable(true);
tree.startEditing(node.getParent(), node.getName());
}
});
tree.setEditable(true);
tree.startEditing(parent, newFolder.getName());
}
private DomainFolder createNewFolderWithDefaultName(DomainFolder parentFolder) {
@ -79,34 +68,10 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext> extends Co
return parentFolder.createFolder(name);
}
catch (InvalidNameException | IOException e) {
throw new AssertException("Unexpected Error creating new folder: "+name, e);
throw new AssertException("Unexpected Error creating new folder: " + name, e);
}
}
/**
* Get folder path as list with top-level folder being first in the list.
* Root folder is not included in list.
* @param folder
* @param folderPathList folder path list
*/
private static final void getFolderPath(DomainFolder folder, List<String> folderPathList) {
if (folder.getParent() != null) {
// don't recurse if we are the root, don't add our 'name' to the list
getFolderPath(folder.getParent(), folderPathList);
folderPathList.add(folder.getName());
}
}
private GTreeNode findNodeForFolder(DataTree tree, DomainFolder newFolder) {
List<String> folderPathList = new ArrayList<>();
getFolderPath(newFolder, folderPathList);
GTreeNode node = tree.getModelRoot();
for (int i = 0; node != null && i < folderPathList.size(); i++) {
node = node.getChild(folderPathList.get(i));
}
return node;
}
private String getNewFolderName(DomainFolder parent) {
String baseName = "NewFolder";
String name = baseName;
@ -128,4 +93,12 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext> extends Co
return file.getParent();
}
private GTreeNode getParentNode(T context) {
GTreeNode node = context.getContextNode();
if (node instanceof DomainFileNode) {
return ((DomainFileNode) node).getParent();
}
return node;
}
}