diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/CreateProjectArchiveAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/CreateProjectArchiveAction.java index e1a8f90c5b..78e10e8bf2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/CreateProjectArchiveAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/CreateProjectArchiveAction.java @@ -4,9 +4,9 @@ * 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. @@ -15,7 +15,6 @@ */ package ghidra.app.plugin.core.datamgr.actions; -import javax.swing.SwingUtilities; import javax.swing.tree.TreePath; import docking.ActionContext; @@ -52,19 +51,15 @@ public class CreateProjectArchiveAction extends DockingAction { } private void selectNewArchive(final Archive archive, final DataTypeArchiveGTree gTree) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - // start an edit on the new temporary node name - GTreeNode node = gTree.getViewRoot(); - final GTreeNode child = node.getChild(archive.getName()); - if (child != null) { - gTree.expandPath(node); - TreePath path = child.getTreePath(); - gTree.scrollPathToVisible(path); - gTree.setSelectedNode(child); - } - } + + GTreeNode root = gTree.getViewRoot(); + gTree.whenNodeIsReady(root, archive.getName(), archiveNode -> { + + gTree.expandPath(root); + + TreePath path = archiveNode.getTreePath(); + gTree.scrollPathToVisible(path); + gTree.setSelectedNode(archiveNode); }); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/DataTypesTreeCellEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/DataTypesTreeCellEditor.java deleted file mode 100644 index ddee9f908c..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/DataTypesTreeCellEditor.java +++ /dev/null @@ -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(); - } - -} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/KeyEntryPanel.java b/Ghidra/Framework/Docking/src/main/java/docking/KeyEntryPanel.java index 424f1ca080..6fac267970 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/KeyEntryPanel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/KeyEntryPanel.java @@ -50,6 +50,11 @@ public class KeyEntryPanel extends JPanel { add(clearButton); } + @Override + public void requestFocus() { + keyEntryField.requestFocus(); + } + /** * Returns the text field used by this class * @return the text field diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java index eaa43231d6..9f0dd49a6a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java @@ -25,8 +25,7 @@ import java.awt.event.*; import java.io.PrintWriter; import java.util.*; import java.util.List; -import java.util.function.Consumer; -import java.util.function.Supplier; +import java.util.function.*; import java.util.stream.Collectors; import javax.swing.*; @@ -316,6 +315,7 @@ public class GTree extends JPanel implements BusyListener { updateModelFilter(); } + // adds the given node to the view when the tree is filtered, regardless of filter match private void ignoreFilter(GTreeNode node) { if (!isFiltered()) { 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 - private void getModelNode(GTreeNode parent, String childName, Consumer consumer) { + private void getModelNode(GTreeNode parent, Predicate matches, + Consumer consumer) { // check for null here to preserve the stack, as the code below is asynchronous Objects.requireNonNull(parent); - Objects.requireNonNull(childName); + Objects.requireNonNull(matches); Objects.requireNonNull(consumer); int expireMs = 3000; Supplier supplier = () -> { GTreeNode modelParent = getModelNode(parent); - if (modelParent != null) { - return modelParent.getChild(childName); + if (modelParent == null) { + return null; } + + List children = modelParent.getChildren(); + for (GTreeNode node : children) { + if (matches.test(node)) { + return node; + } + } + 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 consumer) { + private void getViewNode(GTreeNode parent, Predicate matches, + Consumer consumer) { // check for null here to preserve the stack, as the code below is asynchronous Objects.requireNonNull(parent); - Objects.requireNonNull(childName); + Objects.requireNonNull(matches); Objects.requireNonNull(consumer); int expireMs = 3000; Supplier supplier = () -> { GTreeNode viewParent = getViewNode(parent); - if (viewParent != null) { - return viewParent.getChild(childName); + if (viewParent == null) { + return null; + } + + List children = viewParent.getChildren(); + for (GTreeNode node : children) { + if (matches.test(node)) { + return node; + } } return null; }; 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. + * + *

+ * 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. This method is non-blocking. + * + *

+ * 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. + * + *

+ * 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 matches, + Consumer 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 * available to the model. This method will ensure that the named child passes any current @@ -1144,50 +1223,31 @@ public class GTree extends JPanel implements BusyListener { *

* Note: this method will not wait forever for the given node to appear. It will eventually give * up if the node never arrives. + * + *

+ * 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 * 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 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); - }); + public void whenNodeIsReady(GTreeNode parent, String childName, Consumer consumer) { + Predicate nameMatches = n -> n.getName().equals(childName); + whenNodeIsReady(parent, nameMatches, 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. + *

+ * 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 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 // once the given node has been added to the parent. // + Predicate nameMatches = n -> n.getName().equals(childName); GTreeNode modelParent = getModelNode(parent); - forceNewNodeIntoView(modelParent, childName, viewNode -> { - runTask(new GTreeStartEditingTask(GTree.this, tree, viewNode)); + whenNodeIsReady(modelParent, nameMatches, editNode -> { + runTask(new GTreeStartEditingTask(GTree.this, tree, editNode)); }); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java index 86ab550c52..3260963776 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java @@ -4,9 +4,9 @@ * 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. @@ -185,9 +185,15 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable + * 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 * @return the child with the given name + * @see #getChildren(String) */ public GTreeNode getChild(String name) { for (GTreeNode node : children()) { @@ -198,12 +204,32 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable + * 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 getChildren(String name) { + List 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. * * @param name the name of the child to be returned * @param filter predicate filter * @return the child with the given name + * @see #getChildren(String) */ public GTreeNode getChild(String name, Predicate filter) { for (GTreeNode node : children()) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/tasks/GTreeStartEditingTask.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/tasks/GTreeStartEditingTask.java index c7a298ce28..5adf732d4b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/tasks/GTreeStartEditingTask.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/tasks/GTreeStartEditingTask.java @@ -4,9 +4,9 @@ * 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. @@ -60,7 +60,7 @@ public class GTreeStartEditingTask extends GTreeTask { @Override public void editingCanceled(ChangeEvent e) { cellEditor.removeCellEditorListener(this); - reselectNode(); + tree.setSelectedNode(editNode); // reselect the node on cancel } @Override @@ -68,16 +68,11 @@ public class GTreeStartEditingTask extends GTreeTask { String newName = Objects.toString(cellEditor.getCellEditorValue()); cellEditor.removeCellEditorListener(this); - tree.forceNewNodeIntoView(modelParent, newName, newViewChild -> { - tree.setSelectedNode(newViewChild); + // note: this call only works when the parent cannot have duplicate named nodes + 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);