Merge remote-tracking branch 'origin/GP-5375-dragonmacher-gtree-get-child--SQUASHED'

This commit is contained in:
Ryan Kurtz 2025-02-18 07:15:29 -05:00
commit c8937df382
6 changed files with 155 additions and 199 deletions

View file

@ -15,7 +15,6 @@
*/ */
package ghidra.app.plugin.core.datamgr.actions; package ghidra.app.plugin.core.datamgr.actions;
import javax.swing.SwingUtilities;
import javax.swing.tree.TreePath; import javax.swing.tree.TreePath;
import docking.ActionContext; import docking.ActionContext;
@ -52,19 +51,15 @@ public class CreateProjectArchiveAction extends DockingAction {
} }
private void selectNewArchive(final Archive archive, final DataTypeArchiveGTree gTree) { private void selectNewArchive(final Archive archive, final DataTypeArchiveGTree gTree) {
SwingUtilities.invokeLater(new Runnable() {
@Override GTreeNode root = gTree.getViewRoot();
public void run() { gTree.whenNodeIsReady(root, archive.getName(), archiveNode -> {
// start an edit on the new temporary node name
GTreeNode node = gTree.getViewRoot(); gTree.expandPath(root);
final GTreeNode child = node.getChild(archive.getName());
if (child != null) { TreePath path = archiveNode.getTreePath();
gTree.expandPath(node);
TreePath path = child.getTreePath();
gTree.scrollPathToVisible(path); gTree.scrollPathToVisible(path);
gTree.setSelectedNode(child); gTree.setSelectedNode(archiveNode);
}
}
}); });
} }
} }

View file

@ -1,126 +0,0 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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 ghidra.app.plugin.core.datamgr.editor;
import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin;
import ghidra.app.plugin.core.datamgr.tree.DataTypeNode;
import ghidra.program.model.data.DataType;
import java.awt.Component;
import javax.swing.*;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.tree.*;
import docking.widgets.tree.GTreeNode;
/**
* A implementation of {@link DefaultTreeCellEditor} that adds the ability to launch custom
* editors instead of the default editor. This class will also handle re-selecting the
* edited node after editing has successfully completed.
*/
public class DataTypesTreeCellEditor extends DefaultTreeCellEditor {
private final DataTypeManagerPlugin plugin;
private GTreeNode lastEditedNode;
public DataTypesTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer,
DataTypeManagerPlugin plugin) {
super(tree, renderer);
this.plugin = plugin;
// listener to re-select the edited node after editing is finished (for default editing)
addCellEditorListener(new CellEditorListener() {
@Override
public void editingCanceled(ChangeEvent e) {
lastEditedNode = null;
}
@Override
public void editingStopped(ChangeEvent e) {
if (lastEditedNode != null) {
handleEditingFinished((CellEditor) e.getSource());
}
}
});
}
private void handleEditingFinished(final CellEditor cellEditor) {
// this is called before the changes have been put into place and we
// need to wait until the
// node has been changed before attempting to select it
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Object cellEditorValue = cellEditor.getCellEditorValue();
if (cellEditorValue == null || !(cellEditorValue instanceof String)) {
return;
}
// reselect the cell that was edited
GTreeNode newNode = lastEditedNode.getChild(cellEditorValue.toString());
if (newNode == null) {
return;
}
TreePath path = newNode.getTreePath();
tree.setSelectionPath(path);
tree.scrollPathToVisible(path);
lastEditedNode = null;
}
});
}
@Override
public Component getTreeCellEditorComponent(final JTree jTree, Object value,
boolean isSelected, boolean expanded, boolean leaf, int row) {
if (isCustom(value)) {
edit(value);
SwingUtilities.invokeLater(new Runnable() { // we are going to bring a stand-alone editor
@Override
public void run() { // the tree is not longer involved, so tell it
jTree.cancelEditing();
}
});
return renderer.getTreeCellRendererComponent(jTree, value, isSelected, expanded, leaf,
row, true);
}
lastEditedNode = ((GTreeNode) value).getParent();
return super.getTreeCellEditorComponent(jTree, value, isSelected, expanded, leaf, row);
}
private void edit(Object value) {
DataTypeNode dataTypeNode = (DataTypeNode) value;
if (dataTypeNode.isModifiable()) {
DataType dt = dataTypeNode.getDataType();
plugin.getEditorManager().edit(dt);
}
}
private boolean isCustom(Object value) {
if (!(value instanceof DataTypeNode)) {
return false;
}
DataTypeNode node = (DataTypeNode) value;
return node.hasCustomEditor();
}
}

View file

@ -50,6 +50,11 @@ public class KeyEntryPanel extends JPanel {
add(clearButton); add(clearButton);
} }
@Override
public void requestFocus() {
keyEntryField.requestFocus();
}
/** /**
* Returns the text field used by this class * Returns the text field used by this class
* @return the text field * @return the text field

View file

@ -25,8 +25,7 @@ import java.awt.event.*;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.*;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.swing.*; import javax.swing.*;
@ -316,6 +315,7 @@ public class GTree extends JPanel implements BusyListener {
updateModelFilter(); updateModelFilter();
} }
// adds the given node to the view when the tree is filtered, regardless of filter match
private void ignoreFilter(GTreeNode node) { private void ignoreFilter(GTreeNode node) {
if (!isFiltered()) { if (!isFiltered()) {
return; return;
@ -1086,43 +1086,122 @@ public class GTree extends JPanel implements BusyListener {
} }
// Waits for the given model node, passing it to the consumer when available // Waits for the given model node, passing it to the consumer when available
private void getModelNode(GTreeNode parent, String childName, Consumer<GTreeNode> consumer) { private void getModelNode(GTreeNode parent, Predicate<GTreeNode> matches,
Consumer<GTreeNode> consumer) {
// check for null here to preserve the stack, as the code below is asynchronous // check for null here to preserve the stack, as the code below is asynchronous
Objects.requireNonNull(parent); Objects.requireNonNull(parent);
Objects.requireNonNull(childName); Objects.requireNonNull(matches);
Objects.requireNonNull(consumer); Objects.requireNonNull(consumer);
int expireMs = 3000; int expireMs = 3000;
Supplier<GTreeNode> supplier = () -> { Supplier<GTreeNode> supplier = () -> {
GTreeNode modelParent = getModelNode(parent); GTreeNode modelParent = getModelNode(parent);
if (modelParent != null) { if (modelParent == null) {
return modelParent.getChild(childName); return null;
} }
List<GTreeNode> children = modelParent.getChildren();
for (GTreeNode node : children) {
if (matches.test(node)) {
return node;
}
}
return null; return null;
}; };
ExpiringSwingTimer.get(supplier, expireMs, consumer); ExpiringSwingTimer.get(supplier, expireMs, consumer);
} }
// Waits for the given view node, passing it to the consumer when available // Waits for the given view node, passing it to the consumer when available
private void getViewNode(GTreeNode parent, String childName, Consumer<GTreeNode> consumer) { private void getViewNode(GTreeNode parent, Predicate<GTreeNode> matches,
Consumer<GTreeNode> consumer) {
// check for null here to preserve the stack, as the code below is asynchronous // check for null here to preserve the stack, as the code below is asynchronous
Objects.requireNonNull(parent); Objects.requireNonNull(parent);
Objects.requireNonNull(childName); Objects.requireNonNull(matches);
Objects.requireNonNull(consumer); Objects.requireNonNull(consumer);
int expireMs = 3000; int expireMs = 3000;
Supplier<GTreeNode> supplier = () -> { Supplier<GTreeNode> supplier = () -> {
GTreeNode viewParent = getViewNode(parent); GTreeNode viewParent = getViewNode(parent);
if (viewParent != null) { if (viewParent == null) {
return viewParent.getChild(childName); return null;
}
List<GTreeNode> children = viewParent.getChildren();
for (GTreeNode node : children) {
if (matches.test(node)) {
return node;
}
} }
return null; return null;
}; };
ExpiringSwingTimer.get(supplier, expireMs, consumer); ExpiringSwingTimer.get(supplier, expireMs, consumer);
} }
/**
* 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 matching 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 matches the predicate that returns true when the given node is the desired node
* @param consumer the consumer callback to which the child node will be given when available
*/
public void whenNodeIsReady(GTreeNode parent, Predicate<GTreeNode> matches,
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);
if (modelParent == null) {
Msg.error(this,
"Attempted to show a node with an invalid parent.\n\tParent: " + parent);
return;
}
getModelNode(modelParent, matches, newModelChildren -> {
// force the filter to accept the new node
ignoreFilter(newModelChildren);
// Wait for the view to update from any filtering that may take place
getViewNode(modelParent, matches, consumer);
});
}
/** /**
* A specialized method that will get the child node from the given parent node when it becomes * 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 * available to the model. This method will ensure that the named child passes any current
@ -1145,49 +1224,30 @@ public class GTree extends JPanel implements BusyListener {
* Note: this method will not wait forever for the given node to appear. It will eventually give * Note: this method will not wait forever for the given node to appear. It will eventually give
* up if the node never arrives. * up if the node never arrives.
* *
* <p>
* Note: if your parent node allows duplicate nodes then this method may not match the correct
* node. If that is the case, then use
* {@link #whenNodeIsReady(GTreeNode, Predicate, Consumer)}.
*
* @param parent the model's parent node. If the view's parent node is passed, it will be * @param parent the model's parent node. If the view's parent node is passed, it will be
* translated to the model node. * translated to the model node.
* @param childName the name of the desired child * @param childName the name of the desired child
* @param consumer the consumer callback to which the child node will be given when available * @param consumer the consumer callback to which the child node will be given when available
*/ */
public void forceNewNodeIntoView(GTreeNode parent, String childName, public void whenNodeIsReady(GTreeNode parent, String childName, Consumer<GTreeNode> consumer) {
Consumer<GTreeNode> consumer) { Predicate<GTreeNode> nameMatches = n -> n.getName().equals(childName);
whenNodeIsReady(parent, nameMatches, 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);
if (modelParent == null) {
Msg.error(this, "Attempted to show a node with an invalid parent.\n\tParent: " +
parent + "\n\tchild: " + childName);
return;
}
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 * 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 * 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. * to the parent, up to a timeout period.
* <p>
* Note: if there are multiple nodes by the given name under the given parent, then no editing
* will take place. In that case, you can instead use {@link #startEditing(GTreeNode)}, if you
* have the node. If you have duplicates and do not yet have the node, then you will need to
* create your own mechanism for waiting for the desired node and then starting the edit.
* *
* @param parent the parent node * @param parent the parent node
* @param childName the name of the child to edit * @param childName the name of the child to edit
@ -1206,9 +1266,10 @@ public class GTree extends JPanel implements BusyListener {
// the Swing thread. To deal with this, we use a construct that will run our request // 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. // once the given node has been added to the parent.
// //
Predicate<GTreeNode> nameMatches = n -> n.getName().equals(childName);
GTreeNode modelParent = getModelNode(parent); GTreeNode modelParent = getModelNode(parent);
forceNewNodeIntoView(modelParent, childName, viewNode -> { whenNodeIsReady(modelParent, nameMatches, editNode -> {
runTask(new GTreeStartEditingTask(GTree.this, tree, viewNode)); runTask(new GTreeStartEditingTask(GTree.this, tree, editNode));
}); });
} }

View file

@ -185,9 +185,15 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable<GTre
/** /**
* Returns the child node of this node with the given name. * Returns the child node of this node with the given name.
* <p>
* WARNING: If this node supports duplicate named children, then the node returned by this
* method is arbitrary, depending upon how the nodes are arranged in the parent's list. If you
* know duplicates are not allowed, then calling this method is safe. Otherwise, you should
* instead use {@link #getChildren(String)}.
* *
* @param name the name of the child to be returned * @param name the name of the child to be returned
* @return the child with the given name * @return the child with the given name
* @see #getChildren(String)
*/ */
public GTreeNode getChild(String name) { public GTreeNode getChild(String name) {
for (GTreeNode node : children()) { for (GTreeNode node : children()) {
@ -198,12 +204,32 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable<GTre
return null; return null;
} }
/**
* Gets any children under this parent with the given name.
* <p>
* Note: if you know this parent node does not allow duplicates, then you can use
* {@link #getChild(String)} instead of this method.
*
* @param name the name of the children to be returned
* @return the matching children
*/
public List<GTreeNode> getChildren(String name) {
List<GTreeNode> results = new ArrayList<>();
for (GTreeNode node : children()) {
if (name.equals(node.getName())) {
results.add(node);
}
}
return results;
}
/** /**
* Returns the child node of this node with the given name which satisfies predicate filter. * Returns the child node of this node with the given name which satisfies predicate filter.
* *
* @param name the name of the child to be returned * @param name the name of the child to be returned
* @param filter predicate filter * @param filter predicate filter
* @return the child with the given name * @return the child with the given name
* @see #getChildren(String)
*/ */
public GTreeNode getChild(String name, Predicate<GTreeNode> filter) { public GTreeNode getChild(String name, Predicate<GTreeNode> filter) {
for (GTreeNode node : children()) { for (GTreeNode node : children()) {

View file

@ -60,7 +60,7 @@ public class GTreeStartEditingTask extends GTreeTask {
@Override @Override
public void editingCanceled(ChangeEvent e) { public void editingCanceled(ChangeEvent e) {
cellEditor.removeCellEditorListener(this); cellEditor.removeCellEditorListener(this);
reselectNode(); tree.setSelectedNode(editNode); // reselect the node on cancel
} }
@Override @Override
@ -68,16 +68,11 @@ public class GTreeStartEditingTask extends GTreeTask {
String newName = Objects.toString(cellEditor.getCellEditorValue()); String newName = Objects.toString(cellEditor.getCellEditorValue());
cellEditor.removeCellEditorListener(this); cellEditor.removeCellEditorListener(this);
tree.forceNewNodeIntoView(modelParent, newName, newViewChild -> { // note: this call only works when the parent cannot have duplicate named nodes
tree.setSelectedNode(newViewChild); tree.whenNodeIsReady(modelParent, newName, newNode -> {
tree.setSelectedNode(newNode);
}); });
} }
private void reselectNode() {
String name = editNode.getName();
GTreeNode newModelChild = modelParent.getChild(name);
tree.setSelectedNode(newModelChild);
}
}); });
tree.setNodeEditable(editNode); tree.setNodeEditable(editNode);