diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java index 762394a04b..b252bd652b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java @@ -45,8 +45,7 @@ import ghidra.program.model.symbol.*; import ghidra.program.util.*; import ghidra.util.*; import ghidra.util.exception.*; -import ghidra.util.task.SwingUpdateManager; -import ghidra.util.task.TaskMonitor; +import ghidra.util.task.*; import resources.ResourceManager; public class SymbolTreeProvider extends ComponentProviderAdapter { @@ -76,7 +75,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { */ private List bufferedTasks = new ArrayList<>(); private SwingUpdateManager domainChangeUpdateManager = new SwingUpdateManager(1000, - SwingUpdateManager.DEFAULT_MAX_DELAY, "Symbol Tree Provider", () -> { + AbstractSwingUpdateManager.DEFAULT_MAX_DELAY, "Symbol Tree Provider", () -> { if (bufferedTasks.isEmpty()) { return; @@ -167,7 +166,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { public void mouseClicked(MouseEvent e) { // This code serves to perform navigation in the case that the selection handler - // above does not, as is the case when the node is already selected. This code + // above does not, as is the case when the node is already selected. This code // will get called on the mouse release, whereas the selection handler gets called // on the mouse pressed. // For now, just attempt to perform the goto. It may get called twice, but this @@ -230,12 +229,12 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { String createGroup = "0Create"; int createGroupIndex = 0; - DockingAction createNamespaceAction = new CreateNamespaceAction(plugin, createGroup, - Integer.toString(createGroupIndex++)); - DockingAction createClassAction = new CreateClassAction(plugin, createGroup, - Integer.toString(createGroupIndex++)); - DockingAction convertToClassAction = new ConvertToClassAction(plugin, createGroup, - Integer.toString(createGroupIndex++)); + DockingAction createNamespaceAction = + new CreateNamespaceAction(plugin, createGroup, Integer.toString(createGroupIndex++)); + DockingAction createClassAction = + new CreateClassAction(plugin, createGroup, Integer.toString(createGroupIndex++)); + DockingAction convertToClassAction = + new ConvertToClassAction(plugin, createGroup, Integer.toString(createGroupIndex++)); DockingAction renameAction = new RenameAction(plugin); DockingAction cutAction = new CutAction(plugin, this); @@ -414,6 +413,11 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { } private void rebuildTree() { + + // If we do not cancel the edit here, then an open edits will instead be committed. It + // seems safer to cancel an edit rather than to commit it without asking. + tree.cancelEditing(); + SymbolTreeRootNode node = (SymbolTreeRootNode) tree.getModelRoot(); node.setChildren(null); tree.refilterLater(); @@ -434,9 +438,8 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { private void addTask(GTreeTask task) { // Note: if we want to call this method from off the Swing thread, then we have to // synchronize on the list that we are adding to here. - Swing.assertSwingThread( - "Adding tasks must be done on the Swing thread," + - "since they are put into a list that is processed on the Swing thread. "); + Swing.assertSwingThread("Adding tasks must be done on the Swing thread," + + "since they are put into a list that is processed on the Swing thread. "); bufferedTasks.add(task); domainChangeUpdateManager.update(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolEditor.java index d094d52edf..8ae690cc78 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolEditor.java @@ -19,21 +19,35 @@ import java.awt.Component; import javax.swing.*; +import docking.DockingUtils; +import docking.UndoRedoKeeper; import ghidra.program.model.symbol.Symbol; class SymbolEditor extends DefaultCellEditor { private JTextField symbolField = null; + private UndoRedoKeeper undoRedoKeeper; SymbolEditor() { super(new JTextField()); symbolField = (JTextField) super.getComponent(); symbolField.setBorder(BorderFactory.createEmptyBorder()); + undoRedoKeeper = DockingUtils.installUndoRedo(symbolField); } @Override - public Object getCellEditorValue() { - return symbolField.getText().trim(); + public boolean stopCellEditing() { + if (super.stopCellEditing()) { + undoRedoKeeper.clear(); + return true; + } + return false; + } + + @Override + public void cancelCellEditing() { + super.cancelCellEditing(); + undoRedoKeeper.clear(); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockingUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/DockingUtils.java index 6c8a888f04..e44ac282c9 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockingUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockingUtils.java @@ -57,11 +57,11 @@ import resources.ResourceManager; * When using a UI component that is HTML enabled, care must be used when constructing the text * that is being rendered. *

- * During string-building or concatenation, appending a non-literal string value (ie. - * {@code "Hello " + getFoo();} ), the non-literal string value should be escaped using + * During string-building or concatenation, appending a non-literal string value (ie. + * {@code "Hello " + getFoo();} ), the non-literal string value should be escaped using * {@link HTMLUtilities#escapeHTML(String)} (ie. {@code "Hello " + HTMLUtilities.escapeHTML(getFoo());}. *

- * Of course, there are exceptions to every rule, and if the string value can be definitely be + * Of course, there are exceptions to every rule, and if the string value can be definitely be * traced to its source and there are no user-supplied origins, the HTML escaping can be skipped. *

* Note: just using a UI component that is HTML enabled does not mean that it will treat its @@ -70,9 +70,9 @@ import resources.ResourceManager; * If you fail to do this, the escaped substrings will look wrong because any '<' and '>' chars * (and others) in the substring will be mangled when rendered in plain-text mode. *

- * When working with plain text, try to avoid allowing a user supplied string being the first - * value of text that could be fed to a UI component. This will prevent the possibly hostile - * string from having a leading HTML start tag. + * When working with plain text, try to avoid allowing a user supplied string being the first + * value of text that could be fed to a UI component. This will prevent the possibly hostile + * string from having a leading HTML start tag. * (ie. when displaying an error to the user about a bad file, don't put the filename * value at the start of the string, but instead put a quote or some other delimiter to prevent * html mode). @@ -101,10 +101,10 @@ public class DockingUtils { /** * A version the control key modifiers that is based upon the pre-Java 9 {@link InputEvent} - * usage. This mask is here for those clients that cannot be upgraded, such as those with + * usage. This mask is here for those clients that cannot be upgraded, such as those with * dependencies on 3rd-party libraries that still use the old mask style. - * - * @deprecated use instead {@link #CONTROL_KEY_MODIFIER_MASK} + * + * @deprecated use instead {@link #CONTROL_KEY_MODIFIER_MASK} */ @Deprecated public static final int CONTROL_KEY_MODIFIER_MASK_DEPRECATED = @@ -149,8 +149,8 @@ public class DockingUtils { /** * Checks if the mouseEvent has the "control" key down. On windows, this is actually * the control key. On Mac, it is the command key. - * - * @param mouseEvent the event to check + * + * @param mouseEvent the event to check * @return true if the control key is pressed */ public static boolean isControlModifier(MouseEvent mouseEvent) { @@ -162,8 +162,8 @@ public class DockingUtils { /** * Checks if the mouseEvent has the "control" key down. On windows, this is actually * the control key. On Mac, it is the command key. - * - * @param keyEvent the event to check + * + * @param keyEvent the event to check * @return true if the control key is pressed */ public static boolean isControlModifier(KeyEvent keyEvent) { @@ -172,10 +172,18 @@ public class DockingUtils { return (modifiers & osSpecificMask) == osSpecificMask; } + /** + * Installs key binding support for undo/redo operations on the given text component. + * + *

Note: the edits are tracked by adding a listener to the document of the given text + * component. If that document is changed, then undo/redo will stop working. + * + * @param textComponent the text component + * @return the object that allows the client to track the undo/redo state + */ public static UndoRedoKeeper installUndoRedo(JTextComponent textComponent) { Document document = textComponent.getDocument(); - final UndoRedoKeeper undoRedoKeeper = new UndoRedoKeeper(); document.addUndoableEditListener(e -> { UndoableEdit edit = e.getEdit(); @@ -189,9 +197,11 @@ public class DockingUtils { KeyStroke keyStrokeForEvent = KeyStroke.getKeyStrokeForEvent(e); if (REDO_KEYSTROKE.equals(keyStrokeForEvent)) { undoRedoKeeper.redo(); + e.consume(); // consume to prevent other listeners from processing a second time } else if (UNDO_KEYSTROKE.equals(keyStrokeForEvent)) { undoRedoKeeper.undo(); + e.consume(); // consume to prevent other listeners from processing a second time } } }); @@ -226,7 +236,7 @@ public class DockingUtils { /** * Perform some operation on a component and all of its descendants, recursively - * + * * This traverses the swing/awt component tree starting at the given container and descends * recursively through all containers. Any time a component of type (or subclass of type) is * found, the given callback is executed on it. If order is @@ -234,7 +244,7 @@ public class DockingUtils { * the children of a container before executing the callback on the container itself; if * {@link TreeTraversalOrder#PARENT_FIRST}, then the traversal will execute the callback on the * container before descending. - * + * * The callback must return one of three result values. In normal circumstances, it should * return {@link TreeTraversalResult#CONTINUE}, allowing traversal to continue to the next * element. If the callback wishes to terminate traversal "successfully," e.g., because it @@ -242,14 +252,14 @@ public class DockingUtils { * {@link TreeTraversalResult#FINISH}. If an error occurs during traversal, then it should * either return {@link TreeTraversalResult#TERMINATE} or throw an appropriate exception to * terminate traversal "unsuccessfully." - * + * * This method will also return a value of {@link TreeTraversalResult} indicating how traversal * terminated. If {@link TreeTraversalResult#CONTINUE}, then every element in the subtree was * visited, and traversal was successful. If {@link TreeTraversalResult#FINISH}, then some * elements may have been omitted, but traversal was still successful. If * {@link TreeTraversalResult#TERMINATE}, then some elements may have been omitted, and * traversal was not successful. - * + * * @param start the "root" container of the subtree on which to operate * @param type the type of components on which to operate * @param order whether to operation on children or parents first @@ -288,9 +298,9 @@ public class DockingUtils { /** * Perform some operation on a component and all of its descendents, recursively. - * + * * This applies the operation to all components in the tree, children first. - * + * * @param start the "root" container of the subtree on which to operate * @param cb the callback to perform the actual operation * @return a result indicating whether or not traversal completed successfully @@ -306,18 +316,18 @@ public class DockingUtils { * to be painted. *

* Notes - * Historically, to make a component transparent you would call + * Historically, to make a component transparent you would call * {@link JComponent#setOpaque(boolean)} with a false value. However, it turns out * that the definition and the implementation of this method are at odds. setOpaque(false) * is meant to signal that some part of the component is transparent, so the parent component * needs to be painted. Most LaFs implemented this by not painting the background of the - * component, but used the parent's color instead. The Nimbus LaF actually honors the - * contract of setOpaque(), which has the effect of painting the components + * component, but used the parent's color instead. The Nimbus LaF actually honors the + * contract of setOpaque(), which has the effect of painting the components * background by default. *

- * This method allows components to achieve transparency when they used to + * This method allows components to achieve transparency when they used to * rely on setOpaque(false). - * + * * @param c the component to be made transparent */ public static void setTransparent(JComponent c) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/FilterTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/FilterTextField.java index bac2c1fce1..e6ad4df80d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/FilterTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/FilterTextField.java @@ -22,6 +22,7 @@ import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import docking.DockingUtils; import ghidra.util.SystemUtilities; import ghidra.util.datastruct.WeakDataStructureFactory; import ghidra.util.datastruct.WeakSet; @@ -63,10 +64,10 @@ public class FilterTextField extends JPanel { private WeakSet enterListeners = WeakDataStructureFactory.createCopyOnWriteWeakSet(); /** - * Constructs this text field with the given component. component may be null, but - * then this field will be unable to flash in response to focus events (see the header + * Constructs this text field with the given component. component may be null, but + * then this field will be unable to flash in response to focus events (see the header * documentation). - * + * * @param component The component needed to listen for focus changes, may be null. */ public FilterTextField(Component component) { @@ -74,7 +75,7 @@ public class FilterTextField extends JPanel { } /** - * Constructs this text field with the given component and the preferred visible column + * Constructs this text field with the given component and the preferred visible column * width. component may be null, but then this field will be able to flash in * response to focus events (see the header documentation). * @param component The component needed to listen for focus changes, may be null. @@ -120,6 +121,8 @@ public class FilterTextField extends JPanel { }); add(layeredPane, BorderLayout.NORTH); + + DockingUtils.installUndoRedo(textField); } private void notifyEnterPressed() { @@ -158,11 +161,11 @@ public class FilterTextField extends JPanel { } /** - * This method will signal to the users if a filter is currently applied (has text). For + * This method will signal to the users if a filter is currently applied (has text). For * example, the default implementation will 'flash' the filter by changing its background * color multiple times. *

- * Note: this method will not perform the alert if the minimum time between alerts + * Note: this method will not perform the alert if the minimum time between alerts * has not passed. To force the alter to take place, call {@link #alert(boolean)} with a * value of true. */ @@ -241,11 +244,11 @@ public class FilterTextField extends JPanel { /** * Adds the listener to this filter field that will be called when the user presses the * enter key. - * + * *

Note: this listener cannot be anonymous, as the underlying storage mechanism may be * using a weak data structure. This means that you will need to store the listener in * a field inside of your class. - * + * * @param callback the listener */ public void addEnterListener(Callback callback) { @@ -257,13 +260,13 @@ public class FilterTextField extends JPanel { } /** - * Adds the filter listener to this filter field that will be called when the filter + * Adds the filter listener to this filter field that will be called when the filter * contents change. - * + * *

Note: this listener cannot be anonymous, as the underlying storage mechanism may be * using a weak data structure. This means that you will need to store the listener in * a field inside of your class. - * + * * @param l the listener */ public void addFilterListener(FilterListener l) { @@ -298,7 +301,7 @@ public class FilterTextField extends JPanel { //================================================================================================== // Package Methods (these make testing easier) -//================================================================================================== +//================================================================================================== /*package*/ void doSetBackground(Color c) { textField.setBackground(c); @@ -352,7 +355,7 @@ public class FilterTextField extends JPanel { private void updateFilterButton(boolean showFilter) { - // Note: this must be run on the Swing thread. When the filter button shows itself, + // Note: this must be run on the Swing thread. When the filter button shows itself, // it requires an AWT lock. If called from a non-Swing thread, deadlocks! SystemUtilities.runIfSwingOrPostSwingLater(() -> { if (showFilter) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableTextCellEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableTextCellEditor.java index 9024a1fe93..d30dd9bab0 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableTextCellEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableTextCellEditor.java @@ -1,6 +1,5 @@ /* ### * 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. @@ -18,14 +17,36 @@ package docking.widgets.table; import javax.swing.*; +import docking.DockingUtils; +import docking.UndoRedoKeeper; + public class GTableTextCellEditor extends DefaultCellEditor { private static final Object TABLE_FOCUS_CELL_HIGHLIGHT_BORDER = "Table.focusCellHighlightBorder"; + private UndoRedoKeeper undoRedoKeeper; + public GTableTextCellEditor(JTextField textField) { super(textField); setClickCountToStart(2); textField.setBorder(UIManager.getBorder(TABLE_FOCUS_CELL_HIGHLIGHT_BORDER)); + + undoRedoKeeper = DockingUtils.installUndoRedo(textField); + } + + @Override + public boolean stopCellEditing() { + if (super.stopCellEditing()) { + undoRedoKeeper.clear(); + return true; + } + return false; + } + + @Override + public void cancelCellEditing() { + super.cancelCellEditing(); + undoRedoKeeper.clear(); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnConstraintSet.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnConstraintSet.java index 05f31cb08b..10bf9394b9 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnConstraintSet.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnConstraintSet.java @@ -174,7 +174,7 @@ public class ColumnConstraintSet { return saveState; } - /** + /* * Returns an HTML representation of this constraint set in a tabular form. It will be used * inside the HTML representation of the entire filter. See {@link ColumnBasedTableFilter#getHtmlRepresentation()} * for a description of the table format. diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AbstractColumnConstraintEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AbstractColumnConstraintEditor.java index f92a10765b..0b2052ffdc 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AbstractColumnConstraintEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AbstractColumnConstraintEditor.java @@ -62,7 +62,7 @@ public abstract class AbstractColumnConstraintEditor implements ColumnConstra * This expects the UI to have been constructed. * * @see #getValue() - * @return + * @return the value */ protected abstract ColumnConstraint getValueFromComponent(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java index 068a459ae8..4617f4ecbc 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java @@ -26,6 +26,7 @@ import javax.swing.*; import org.apache.commons.lang3.StringUtils; +import docking.DockingUtils; import docking.widgets.DropDownTextField; import docking.widgets.DropDownTextFieldDataModel; import docking.widgets.list.GListCellRenderer; @@ -61,6 +62,7 @@ public class AutocompletingStringConstraintEditor extends DataLoadingConstraintE textField = new DropDownTextField<>(autocompleter, 100); textField.setIgnoreEnterKeyPress(true); textField.getDocument().addUndoableEditListener(e -> valueChanged()); + DockingUtils.installUndoRedo(textField); panel.add(textField, BorderLayout.NORTH); textField.addActionListener(e -> textField.closeDropDownWindow()); @@ -139,9 +141,8 @@ public class AutocompletingStringConstraintEditor extends DataLoadingConstraintE return Collections.emptyList(); } searchText = searchText.trim(); - lastConstraint = - (StringColumnConstraint) currentConstraint.parseConstraintValue(searchText, - columnDataSource.getTableDataSource()); + lastConstraint = (StringColumnConstraint) currentConstraint + .parseConstraintValue(searchText, columnDataSource.getTableDataSource()); // Use a Collator to support languages other than English. Collator collator = Collator.getInstance(); @@ -222,8 +223,7 @@ public class AutocompletingStringConstraintEditor extends DataLoadingConstraintE // escape all unescaped '\' and '$' chars, as Match.appendReplacement() will treat // them as regex characters String quoted = Matcher.quoteReplacement(group); - String replacement = - HTMLUtilities.colorString(color, HTMLUtilities.bold(quoted)); + String replacement = HTMLUtilities.colorString(color, HTMLUtilities.bold(quoted)); matcher.appendReplacement(sb, replacement); } matcher.appendTail(sb); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/StringConstraintEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/StringConstraintEditor.java index e9202f8458..d3d048dc6a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/StringConstraintEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/StringConstraintEditor.java @@ -19,6 +19,7 @@ import java.awt.*; import javax.swing.*; +import docking.DockingUtils; import docking.widgets.label.GDHtmlLabel; import docking.widgets.table.constraint.ColumnConstraint; import docking.widgets.table.constraint.StringColumnConstraint; @@ -50,6 +51,8 @@ public class StringConstraintEditor extends AbstractColumnConstraintEditor valueChanged()); + DockingUtils.installUndoRedo(textField); + panel.add(textField, BorderLayout.CENTER); infoLabel = new GDHtmlLabel("abc"); // temporary text in the label so that it sizes properly @@ -77,7 +80,7 @@ public class StringConstraintEditor extends AbstractColumnConstraintEditor * Note: See the usage note at the header of this class concerning how tree state is used * relative to the equals() method. - * + * * @return the saved state */ public GTreeState getTreeState() { @@ -538,7 +538,7 @@ public class GTree extends JPanel implements BusyListener { /** * Returns the model for this tree - * + * * @return the model for this tree */ public GTreeModel getModel() { @@ -554,7 +554,7 @@ public class GTree extends JPanel implements BusyListener { /** * Returns the current viewport position of the scrollable tree. - * + * * @return the current viewport position of the scrollable tree. */ public Point getViewPosition() { @@ -825,7 +825,7 @@ public class GTree extends JPanel implements BusyListener { * 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}. - * + * * @return the root node as provided by the client. */ public GTreeNode getModelRoot() { @@ -837,7 +837,7 @@ public class GTree extends JPanel implements BusyListener { * 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. - * + * * @return the root node currently being display by the {@link JTree} */ public GTreeNode getViewRoot() { @@ -846,7 +846,7 @@ public class GTree extends JPanel implements BusyListener { /** * This method is useful for debugging tree problems. Don't know where else to put it. - * + * * @param out the output writer * @param name use this to indicate what tree event occurred ("node inserted" "node removed", * etc.) @@ -1009,6 +1009,11 @@ 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) { + // check for null here to preserve the stack, as the code below is asynchronous + Objects.requireNonNull(parent); + Objects.requireNonNull(childName); + Objects.requireNonNull(consumer); + int expireMs = 3000; Supplier supplier = () -> { GTreeNode modelParent = getModelNode(parent); @@ -1023,6 +1028,11 @@ public class GTree extends JPanel implements BusyListener { // Waits for the given view node, passing it to the consumer when available private void getViewNode(GTreeNode parent, String childName, Consumer consumer) { + // check for null here to preserve the stack, as the code below is asynchronous + Objects.requireNonNull(parent); + Objects.requireNonNull(childName); + Objects.requireNonNull(consumer); + int expireMs = 3000; Supplier supplier = () -> { GTreeNode viewParent = getViewNode(parent); @@ -1081,6 +1091,11 @@ public class GTree extends JPanel implements BusyListener { // 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); @@ -1252,7 +1267,7 @@ public class GTree extends JPanel implements BusyListener { /** * Re-filters the tree if the newNode should be included in the current filter results. If the * new node doesn't match the filter, there is no need to refilter the tree. - * + * * @param newNode the node that may cause the tree to refilter. */ public void refilterLater(GTreeNode newNode) { @@ -1300,7 +1315,7 @@ public class GTree extends JPanel implements BusyListener { * Used to run simple GTree tasks that can be expressed as a {@link MonitoredRunnable} (or a * lambda taking a {@link TaskMonitor}). *

- * + * * @param runnableTask {@link TaskMonitor} to watch and update with progress. */ public void runTask(MonitoredRunnable runnableTask) { @@ -1324,6 +1339,10 @@ public class GTree extends JPanel implements BusyListener { tree.stopEditing(); } + public void cancelEditing() { + tree.cancelEditing(); + } + public void setNodeEditable(GTreeNode child) { // for now only subclasses of GTree will set a node editable. } @@ -1522,7 +1541,7 @@ public class GTree extends JPanel implements BusyListener { * Calling setSelectedPaths on GTree queues the selection for after any currently scheduled * tasks. This method sets the selected path immediately and does not wait for for scheduled * tasks. - * + * * @param path the path to select. */ @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeCellEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeCellEditor.java index e22f82fc3a..35c3d22f2a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeCellEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeCellEditor.java @@ -1,6 +1,5 @@ /* ### * 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. @@ -18,22 +17,56 @@ package docking.widgets.tree.support; import java.awt.Component; -import javax.swing.JTree; +import javax.swing.*; import javax.swing.tree.DefaultTreeCellEditor; import javax.swing.tree.DefaultTreeCellRenderer; +import docking.DockingUtils; +import docking.UndoRedoKeeper; import docking.widgets.tree.GTreeNode; public class GTreeCellEditor extends DefaultTreeCellEditor { + + private UndoRedoKeeper undoRedoKeeper; + public GTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer) { super(tree, renderer); - } - - @Override - public Component getTreeCellEditorComponent(JTree jTree, Object value, - boolean isSelected, boolean expanded, boolean leaf, int row) { - GTreeNode node = (GTreeNode)value; + if (realEditor instanceof DefaultCellEditor) { + Component c = ((DefaultCellEditor) realEditor).getComponent(); + if (c instanceof JTextField) { + JTextField tf = (JTextField) c; + undoRedoKeeper = DockingUtils.installUndoRedo(tf); + } + } + } + + @Override + public boolean stopCellEditing() { + if (super.stopCellEditing()) { + clearUndoRedo(); + return true; + } + return false; + } + + @Override + public void cancelCellEditing() { + super.cancelCellEditing(); + clearUndoRedo(); + } + + private void clearUndoRedo() { + if (undoRedoKeeper != null) { + undoRedoKeeper.clear(); + } + } + + @Override + public Component getTreeCellEditorComponent(JTree jTree, Object value, boolean isSelected, + boolean expanded, boolean leaf, int row) { + + GTreeNode node = (GTreeNode) value; if (node.isLeaf()) { renderer.setLeafIcon(node.getIcon(expanded)); } @@ -41,8 +74,8 @@ public class GTreeCellEditor extends DefaultTreeCellEditor { renderer.setOpenIcon(node.getIcon(true)); renderer.setClosedIcon(node.getIcon(false)); } - return super.getTreeCellEditorComponent(jTree, value, isSelected, expanded, - leaf, row); + + return super.getTreeCellEditorComponent(jTree, value, isSelected, expanded, leaf, row); } }