mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 19:42:36 +02:00
Merge remote-tracking branch 'origin/GP-1615_ghidravore_allowing_some_nodes_to_resist_filtering'
This commit is contained in:
commit
cc6293a10c
15 changed files with 387 additions and 227 deletions
|
@ -129,6 +129,7 @@ public class CreateTypeDefAction extends AbstractTypeDefAction {
|
|||
|
||||
GTreeNode finalParentNode = info.getParentNode();
|
||||
String newNodeName = newTypeDef.getName();
|
||||
dataTypeManager.flushEvents();
|
||||
gTree.startEditing(finalParentNode, newNodeName);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()));
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue