diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/HexBigIntegerTableCellEditor.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/HexBigIntegerTableCellEditor.java index a5b685e578..2b1edec06b 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/HexBigIntegerTableCellEditor.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/HexBigIntegerTableCellEditor.java @@ -51,7 +51,7 @@ public class HexBigIntegerTableCellEditor extends AbstractCellEditor implements input.getComponent().setBorder(UIManager.getBorder("Table.focusCellHighlightBorder")); input.setMinValue(null); // allow negative numbers input.setFormat(IntegerFormat.HEX); - input.setUseNumberPrefix(false); + input.setAutoSwitchMode(false); input.setShowNumberMode(true); input.setHorizontalAlignment(SwingConstants.RIGHT); diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimSearchDialog.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimSearchDialog.java index 89f1389d7f..6178e308f2 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimSearchDialog.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimSearchDialog.java @@ -17,16 +17,18 @@ package ghidra.features.bsim.gui.search.dialog; import java.awt.BorderLayout; import java.awt.Component; -import java.math.BigInteger; import java.util.List; import java.util.Set; +import java.util.function.Predicate; import javax.swing.*; import docking.DialogComponentProvider; import docking.DockingWindowManager; import docking.widgets.EmptyBorderButton; -import docking.widgets.textfield.IntegerTextField; +import docking.widgets.numberformat.IntegerFormatter; +import docking.widgets.numberformat.IntegerFormatterFactory; +import docking.widgets.textfield.GFormattedTextField; import generic.theme.GIcon; import ghidra.app.services.GoToService; import ghidra.features.bsim.gui.BSimSearchPlugin; @@ -49,7 +51,7 @@ public class BSimSearchDialog extends AbstractBSimSearchDialog { private BSimFilterPanel filterPanel; // Query Settings - private IntegerTextField maxResultsField; + private GFormattedTextField maxResultsField; public BSimSearchDialog(PluginTool tool, BSimSearchService service, BSimServerManager serverManager, Set functions) { @@ -124,26 +126,34 @@ public class BSimSearchDialog extends AbstractBSimSearchDialog { protected JPanel buildOptionsPanel() { JPanel panel = super.buildOptionsPanel(); - maxResultsField = new IntegerTextField(10); - maxResultsField.setValue(100); - maxResultsField.setMinValue(BigInteger.ONE); - maxResultsField.setUseNumberPrefix(false); - maxResultsField.setShowNumberMode(false); - - JComponent maxResultsComponent = maxResultsField.getComponent(); + IntegerFormatter formatter = new OneOrMoreIntegerFormatter(); + IntegerFormatterFactory factory = new IntegerFormatterFactory(formatter, false); + maxResultsField = new GFormattedTextField(factory, 100); JLabel maxLabel = new JLabel("Max Matches Per Function:"); - maxLabel.setLabelFor(maxResultsComponent); + maxLabel.setLabelFor(maxResultsField); panel.add(maxLabel); - panel.add(maxResultsComponent); + panel.add(maxResultsField); return panel; } + private class OneOrMoreIntegerFormatter extends IntegerFormatter { + + @Override + protected Predicate createIsValidNumberPredicate() { + return n -> { + int i = n.intValue(); + return i > 0; + }; + } + + } + protected BSimSearchSettings getSearchSettings() { double similarity = similarityField.getValue(); double confidence = confidenceField.getValue(); - int maxResults = maxResultsField.getIntValue(); + int maxResults = (Integer) maxResultsField.getValue(); BSimFilterSet set = filterPanel.getFilterSet(); return new BSimSearchSettings(similarity, confidence, maxResults, set); } diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/StructureEditor.htm b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/StructureEditor.htm index 2b0c9c4cf0..5c7e42ccba 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/StructureEditor.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/StructureEditor.htm @@ -1215,6 +1215,16 @@ start the search from there.

+ + +

Go To Offset

+ +
+

+ Go to Offset is available from the popup menu. It allows you to jump to the row with + the given offset. +

+

Showing a Component's Data Type diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorModel.java index 189c4084c7..d2c47818b7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorModel.java @@ -141,7 +141,7 @@ abstract public class CompositeEditorModel extends Composit } if (dataType.isDeleted()) { - // This can occur when mayny events get lumped together and a change event triggers + // This can occur when many events get lumped together and a change event triggers // a delayed reload prior to datatype removal and its event if (dataType == originalComposite) { // Re-route to dataTypeRemoved callback after restoring listener. @@ -1480,12 +1480,4 @@ abstract public class CompositeEditorModel extends Composit return (viewComposite instanceof Structure) || (viewComposite instanceof Union); } - /** - * Get the composite edtor's datatype manager - * @return composite edtor's datatype manager - */ - public CompositeViewerDataTypeManager getViewDataTypeManager() { - return viewDTM; - } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorPanel.java index f475d55194..10121cbfcc 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorPanel.java @@ -952,61 +952,7 @@ public abstract class CompositeEditorPanel isUndefined(dtc); - int undefinedRow = findNextMatchingDtc(currentRow, forward, isUndefined); - int startRow = undefinedRow + (forward ? 1 : -1); - int n = model.getRowCount(); - if (startRow >= n) { - return null; - } - - DtcMatcher isDefined = dtc -> !isUndefined(dtc); - return findNextMatchingDtc(startRow, forward, isDefined); - } - - private int findNextMatchingDtc(int row, boolean forward, DtcMatcher matcher) { - - int start = row; - int end = forward ? model.getRowCount() : -1; - int direction = forward ? 1 : -1; - for (int i = start; i != end; i += direction) { - DataTypeComponent dtc = model.getComponent(i); - if (matcher.matches(dtc)) { - return i; - } - } - return -1; - } - - // just a nicer predicate - private interface DtcMatcher { - public boolean matches(DataTypeComponent dtc); - } - - private boolean isUndefined(DataTypeComponent dtc) { - if (dtc == null) { - return true; - } - - DataType dt = dtc.getDataType(); - return Undefined.isUndefined(dt); - } - - private void goToRow(int row) { + protected void goToRow(int row) { table.getSelectionModel().setSelectionInterval(row, row); Rectangle cellRect = table.getCellRect(row, 0, true); table.scrollRectToVisible(cellRect); @@ -1601,4 +1547,5 @@ public abstract class CompositeEditorPanel extends AbstractTableMo return viewComposite.isPackingEnabled(); } + /** + * Get the composite edtor's datatype manager + * @return composite edtor's datatype manager + */ + public CompositeViewerDataTypeManager getViewDataTypeManager() { + return viewDTM; + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/JumpToOffsetAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/JumpToOffsetAction.java new file mode 100644 index 0000000000..aa6c5f176c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/JumpToOffsetAction.java @@ -0,0 +1,62 @@ +/* ### + * 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 ghidra.app.plugin.core.compositeeditor; + +import docking.ActionContext; +import docking.action.MenuData; +import docking.widgets.dialogs.NumberInputDialog; +import docking.widgets.textfield.integer.IntegerFormat; +import ghidra.util.HelpLocation; + +public class JumpToOffsetAction extends CompositeEditorTableAction { + + protected JumpToOffsetAction(StructureEditorProvider provider) { + super(provider, "Jump to Offset"); + + MenuData data = new MenuData(new String[] { "Jump to Offset" }); + data.setMenuGroup(BASIC_ACTION_GROUP + "_Jump"); + data.setMenuSubGroup("3"); + setPopupMenuData(data); + + setHelpLocation( + new HelpLocation(provider.getHelpTopic(), "Structure_Editor_Go_To_Offset")); + } + + @Override + public void actionPerformed(ActionContext context) { + NumberInputDialog dialog = new NumberInputDialog("Offset", 0, 0, Integer.MAX_VALUE); + dialog.setHelpLocation(getHelpLocation()); + dialog.setTitle("Enter Offset"); + dialog.setPrompt("Enter offset:"); + + // make the 0x optional; users will have to manually change modes + dialog.setAutoSwitchMode(false); + boolean isHex = model.isShowingNumbersInHex(); + if (isHex) { + dialog.setMode(IntegerFormat.HEX); + dialog.setInputAsText(""); // have no initial value + dialog.setDefaultMessage(""); + } + + if (!dialog.show()) { + return; // cancelled + } + + int offset = dialog.getValue(); + ((StructureEditorProvider) provider).goToOffset(offset); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/NextPrevDefinedComponentAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/NextPrevDefinedComponentAction.java index c62ce4747c..b99bc769f9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/NextPrevDefinedComponentAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/NextPrevDefinedComponentAction.java @@ -27,13 +27,14 @@ public class NextPrevDefinedComponentAction extends CompositeEditorTableAction { private boolean forward; - public NextPrevDefinedComponentAction(CompositeEditorProvider provider, boolean forward) { + public NextPrevDefinedComponentAction(StructureEditorProvider provider, boolean forward) { super(provider, getName(forward)); this.forward = forward; MenuData data = new MenuData(new String[] { getName(forward) }); - data.setMenuGroup(BASIC_ACTION_GROUP + "_2"); // put below the basic action group + data.setMenuGroup(BASIC_ACTION_GROUP + "_Jump"); // put below the basic action group + data.setMenuSubGroup("1"); setPopupMenuData(data); setKeyBindingData(new KeyBindingData(forward ? "Control Down" : "Control Up")); @@ -44,7 +45,7 @@ public class NextPrevDefinedComponentAction extends CompositeEditorTableAction { @Override public void actionPerformed(ActionContext context) { - provider.goToNextDefinedRow(forward); + ((StructureEditorProvider) provider).goToNextDefinedRow(forward); } private static String getName(boolean forward) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorModel.java index 817f989be2..6f9c1c398f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorModel.java @@ -31,7 +31,8 @@ import docking.widgets.table.GTableHeaderRenderer; import ghidra.program.model.data.*; import ghidra.program.model.lang.InsufficientBytesException; import ghidra.util.Msg; -import ghidra.util.exception.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.UsrException; import ghidra.util.task.TaskLauncher; import ghidra.util.task.TaskMonitor; @@ -201,6 +202,15 @@ class StructureEditorModel extends CompEditorModel { return viewComposite == null ? 0 : viewComposite.getNumComponents(); } + public int getRowForOffset(int offset) { + DataTypeComponent dtc = viewComposite.getComponentContaining(offset); + if (dtc != null) { + return dtc.getOrdinal(); + } + + return 0; + } + @Override protected boolean isSizeEditable() { return !isPackingEnabled(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorPanel.java index 54dfa9366b..8cba67b9e1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorPanel.java @@ -18,8 +18,7 @@ package ghidra.app.plugin.core.compositeeditor; import java.awt.Component; import docking.DockingWindowManager; -import ghidra.program.model.data.DataTypeComponent; -import ghidra.program.model.data.Structure; +import ghidra.program.model.data.*; /** * Editor panel for Union datatype @@ -53,4 +52,66 @@ public class StructureEditorPanel extends CompEditorPanel= 0) { + goToRow(row); + } + } + + void goToNextDefinedRow(boolean forward) { + + Integer nextRow = findNextDefinedRow(forward); + if (nextRow == null) { + getToolkit().beep(); + } + else { + goToRow(nextRow); + } + } + + private Integer findNextDefinedRow(boolean forward) { + + int currentRow = Math.max(0, model.getRow()); + DtcMatcher isUndefined = dtc -> isUndefined(dtc); + int undefinedRow = findNextMatchingDtc(currentRow, forward, isUndefined); + int startRow = undefinedRow + (forward ? 1 : -1); + int n = model.getRowCount(); + if (startRow >= n) { + return null; + } + + DtcMatcher isDefined = dtc -> !isUndefined(dtc); + return findNextMatchingDtc(startRow, forward, isDefined); + } + + private int findNextMatchingDtc(int row, boolean forward, DtcMatcher matcher) { + + int start = row; + int end = forward ? model.getRowCount() : -1; + int direction = forward ? 1 : -1; + for (int i = start; i != end; i += direction) { + DataTypeComponent dtc = model.getComponent(i); + if (matcher.matches(dtc)) { + return i; + } + } + return -1; + } + + // just a nicer predicate + private interface DtcMatcher { + public boolean matches(DataTypeComponent dtc); + } + + private boolean isUndefined(DataTypeComponent dtc) { + if (dtc == null) { + return true; + } + + DataType dt = dtc.getDataType(); + return Undefined.isUndefined(dt); + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorProvider.java index c227ef4ee8..aca142b33a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorProvider.java @@ -87,6 +87,7 @@ public class StructureEditorProvider new ShowDataTypeInTreeAction(this), new NextPrevDefinedComponentAction(this, true), new NextPrevDefinedComponentAction(this, false), + new JumpToOffsetAction(this), // new ViewBitFieldAction(this) }; @@ -165,4 +166,14 @@ public class StructureEditorProvider } return null; } + + public void goToNextDefinedRow(boolean forward) { + ((StructureEditorPanel) editorPanel).goToNextDefinedRow(forward); + } + + public void goToOffset(int offset) { + ((StructureEditorPanel) editorPanel).goToOffset(offset); + } + + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/AddBlockDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/AddBlockDialog.java index d1dcf36172..c626a6bbf9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/AddBlockDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/AddBlockDialog.java @@ -477,14 +477,14 @@ class AddBlockDialog extends DialogComponentProvider implements ChangeListener { schemeDestByteCountField = new IntegerTextField(4, 1); schemeDestByteCountField.setMinValue(BigInteger.ZERO); - schemeDestByteCountField.setUseNumberPrefix(false); + schemeDestByteCountField.setAutoSwitchMode(false); schemeDestByteCountField.setFormat(IntegerFormat.DEC); schemeDestByteCountField.addChangeListener(ev -> schemeDestByteCountChanged()); schemeDestByteCountField.setAccessibleName("Mapping Ratio: Destination Size"); schemeSrcByteCountField = new IntegerTextField(4, 1); schemeSrcByteCountField.setMinValue(BigInteger.ZERO); - schemeSrcByteCountField.setUseNumberPrefix(false); + schemeSrcByteCountField.setAutoSwitchMode(false); schemeSrcByteCountField.setFormat(IntegerFormat.DEC); schemeSrcByteCountField.addChangeListener(ev -> schemeSrcByteCountChanged()); schemeSrcByteCountField.setAccessibleName("Mapping Ratio: Source Size"); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/AbstractNumberInputDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/AbstractNumberInputDialog.java index 71357bccec..94d43d9995 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/AbstractNumberInputDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/AbstractNumberInputDialog.java @@ -24,6 +24,7 @@ import docking.DialogComponentProvider; import docking.DockingWindowManager; import docking.widgets.label.GDLabel; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.AbstractIntegerTextField; import docking.widgets.textfield.integer.IntegerFormat; import ghidra.util.Swing; @@ -64,9 +65,7 @@ public abstract class AbstractNumberInputDialog extends DialogComponentProvider * @param showAsHex if true, the initial value will be displayed as hex */ public AbstractNumberInputDialog(String title, String prompt, BigInteger initialValue, - BigInteger min, - BigInteger max, - boolean showAsHex) { + BigInteger min, BigInteger max, boolean showAsHex) { super(title, true, true, true, false); this.min = min; @@ -204,10 +203,12 @@ public abstract class AbstractNumberInputDialog extends DialogComponentProvider private void selectAndFocusText() { Swing.runLater(() -> { - numberInputField.requestFocus(); - numberInputField.selectAll(); - }); + if (numberInputField.isSelectTextOnFocusGainedEnabled()) { + numberInputField.requestFocus(); + numberInputField.selectAll(); + } + }); } /** @@ -228,6 +229,41 @@ public abstract class AbstractNumberInputDialog extends DialogComponentProvider numberInputField.setValue(value); } + /** + * Sets the fields value as text. + * @param text the text + */ + public void setInputAsText(String text) { + numberInputField.setText(text); + } + + /** + * Sets the input mode for the field in this dialog. + * @param mode the mode + * @see AbstractIntegerTextField#setFormat(IntegerFormat) + */ + public void setMode(IntegerFormat mode) { + numberInputField.setFormat(mode); + } + + /** + * Sets whether this text field will switch the input mode format when typing a matching prefix. + * + * @param autoSwitch true to auto-switch; false requires user to change modes manually + * @see AbstractIntegerTextField#setAutoSwitchMode(boolean) + */ + public void setAutoSwitchMode(boolean autoSwitch) { + numberInputField.setAutoSwitchMode(autoSwitch); + } + + /** + * Sets the label text of this dialog. The default value is "Enter number of items". + * @param text the text + */ + public void setPrompt(String text) { + label.setText(text); + } + /** * Sets the default message to be displayed when valid values are in the text fields. * @param defaultMessage the message to be displayed when valid values are in the text fields. diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/NumberInputDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/NumberInputDialog.java index 0409f5285a..26e20572e0 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/NumberInputDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/NumberInputDialog.java @@ -96,5 +96,4 @@ public class NumberInputDialog extends AbstractNumberInputDialog { public int getValue() { return getIntValue(); } - } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/numberformat/IntegerFormatter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/numberformat/IntegerFormatter.java index 51463c3a02..57c95ca34d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/numberformat/IntegerFormatter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/numberformat/IntegerFormatter.java @@ -17,6 +17,7 @@ package docking.widgets.numberformat; import java.awt.Toolkit; import java.text.*; +import java.util.function.Predicate; import javax.swing.text.*; @@ -36,18 +37,21 @@ public class IntegerFormatter extends NumberFormatter { setAllowsInvalid(true); } + protected Predicate createIsValidNumberPredicate() { + return n -> true; + } + @Override protected DocumentFilter getDocumentFilter() { if (myDocumentFilter == null) { myDocumentFilter = createDocumentFilter(); } - return myDocumentFilter; } protected DocumentFilter createDocumentFilter() { return new PosiviteValueIntegerDocumentFilterWrapper(getFormat(), - getOriginalDocumentFilter()); + getOriginalDocumentFilter(), createIsValidNumberPredicate()); } protected DocumentFilter getOriginalDocumentFilter() { @@ -58,10 +62,17 @@ public class IntegerFormatter extends NumberFormatter { protected final DocumentFilter wrappedFilter; protected final Format format; + protected final Predicate isValidNumber; PosiviteValueIntegerDocumentFilterWrapper(Format format, DocumentFilter wrappedFilter) { + this(format, wrappedFilter, n -> true); + } + + PosiviteValueIntegerDocumentFilterWrapper(Format format, DocumentFilter wrappedFilter, + Predicate isValidNumber) { this.format = format; this.wrappedFilter = wrappedFilter; + this.isValidNumber = isValidNumber; } @Override @@ -132,6 +143,10 @@ public class IntegerFormatter extends NumberFormatter { return false; } + if (!isValidNumber.test(number)) { + return false; + } + Long longValue = number.longValue(); if (longValue.compareTo(0L) < 0) { return false; // no negatives diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/IntegerTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/IntegerTextField.java index d81c3feb7e..0e9af79f80 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/IntegerTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/IntegerTextField.java @@ -44,17 +44,14 @@ import docking.widgets.textfield.integer.IntegerFormat; * equal to this value. *
  • Min value - This value must be generally be negative and will restrict the input to values * greater than or equal to this value. As a special case, the min value can be set to 1.
  • - *
  • Use number prefix - If this mode is on, then non-decimal values must be typed with its - * prefix(i.e., 0x for hex). When requiring non-decimal prefix, the field is permitted to auto + *
  • Auto-switch mode - When requiring non-decimal prefix, the field is permitted to auto * switch formats based on the prefix (or lack thereof). When the use prefix is off, the only way * to switch formats is to use the ctrl-M action.
  • *
  • Show the number mode as hint text - If showing number mode is on, the format short name * is displayed lightly in the bottom right portion of the text field. * See {@link #setShowNumberMode(boolean)}
  • * - * */ - public class IntegerTextField extends AbstractIntegerTextField { private IntegerFormat hex; private IntegerFormat decimal; @@ -137,11 +134,11 @@ public class IntegerTextField extends AbstractIntegerTextField { * mode and the typed text will be interpreted appropriately for the mode. * * @param usePrefix true to use the 0x convention for hex. - * @deprecated use {@link #setUseNumberPrefix(boolean)} instead + * @deprecated use {@link #setAutoSwitchMode(boolean)} instead */ @Deprecated(forRemoval = true, since = "12.2") public void setAllowsHexPrefix(boolean usePrefix) { - setUseNumberPrefix(usePrefix); + setAutoSwitchMode(usePrefix); } /** diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/AbstractIntegerTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/AbstractIntegerTextField.java index e3ab39fe7f..5b08fb5b42 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/AbstractIntegerTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/AbstractIntegerTextField.java @@ -25,6 +25,9 @@ import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.text.*; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; + /** * Base class for IntegerTextFields that allow entering integer values based on some * integer format (i.e., hex, decimal, unsigned hex, binary, etc.). This field does input @@ -38,7 +41,7 @@ public class AbstractIntegerTextField { protected IntegerFormat currentFormat; private BigInteger minValue; private BigInteger maxValue; - private boolean usePrefix = true; + private boolean autoSwitch = true; /** * Creates a new IntegerTextField with the specified number of columns and initial value @@ -53,7 +56,7 @@ public class AbstractIntegerTextField { allFormats = Arrays.asList(formats); currentFormat = allFormats.get(0); - textField = new MultiFormatTextField(columns, allFormats, m -> setFormat(m)); + textField = new CustomHintMultiFormatTextField(columns); AbstractDocument document = (AbstractDocument) textField.getDocument(); document.setDocumentFilter(new HexDecimalDocumentFilter()); @@ -61,6 +64,14 @@ public class AbstractIntegerTextField { textField.addTextChangedCallback(this::valueChanged); } + /** + * Sets the accessible name for the component of this input field. + * @param name the accessible name for this field + */ + public void setAccessibleName(String name) { + textField.getAccessibleContext().setAccessibleName(name); + } + /** * Adds a change listener that will be notified whenever the value changes. * @@ -70,14 +81,6 @@ public class AbstractIntegerTextField { listeners.add(listener); } - /** - * Sets the accessible name for the component of this input field. - * @param name the accessible name for this field - */ - public void setAccessibleName(String name) { - textField.getAccessibleContext().setAccessibleName(name); - } - /** * Removes the changes listener. * @@ -87,6 +90,17 @@ public class AbstractIntegerTextField { listeners.remove(listener); } + /** + * {@return true if the text field will select the text when it initially gains focus.} + */ + public boolean isSelectTextOnFocusGainedEnabled() { + Object value = textField.getClientProperty("JTextField.selectAllOnFocusPolicy"); + if (value == null) { + return false; + } + return Strings.CI.containsAny(value.toString(), "once", "always"); + } + /** * Returns the current value of the field or null if the field has no current value. * @@ -94,7 +108,11 @@ public class AbstractIntegerTextField { */ public BigInteger getValue() { String text = textField.getText(); - return parse(text, currentFormat); + BigInteger value = parse(text, currentFormat); + if (isInBounds(value)) { + return value; + } + return null; } /** @@ -198,9 +216,14 @@ public class AbstractIntegerTextField { } private String addPrefix(String text) { + + // 'autoSwitch' requires a prefix. When not using auto-switch, do not add a prefix if the + // user has not added one. + boolean usePrefix = autoSwitch; if (!usePrefix) { return text; } + String prefix = currentFormat.getPrefix(); if (prefix.isBlank()) { return text; @@ -328,6 +351,18 @@ public class AbstractIntegerTextField { textField.selectAll(); } + /** + * Clears the current selection and places the caret at the end of the text. + */ + public void clearSelection() { + String text = textField.getText(); + int end = 0; + if (!StringUtils.isBlank(text)) { + end = text.length(); + } + textField.setCaretPosition(end); + } + /** * Sets the horizontal alignment of the JTextField * @@ -338,16 +373,17 @@ public class AbstractIntegerTextField { } /** - * Sets whether or not that non-decimal formats require using a prefix (i.e., "0x" for hex). - * Generally, using a prefix is preferred as it allows the mode to auto-switch as the user - * types (or not types) a prefix. If the prefix is not used, the only way to change input - * formats is to use the built-in cntr-M action. - * @param usePrefix true to require a prefix, false to not require a prefix + * Sets whether this text field will switch the input mode format when typing a matching prefix. + * For example, assuming DEC and HEX modes are available, while in DEC mode, typing {@code 0x} + * will switch the mode format to HEX. + *

    + * Auto-switching is on by default. + * + * @param newAutoSwitch true to auto-switch; false requires user to change modes manually */ - public void setUseNumberPrefix(boolean usePrefix) { - BigInteger value = getValue(); - this.usePrefix = usePrefix; - setValue(value); + public void setAutoSwitchMode(boolean newAutoSwitch) { + this.autoSwitch = newAutoSwitch; + textField.repaint(); } /** @@ -358,13 +394,35 @@ public class AbstractIntegerTextField { return new ArrayList<>(allFormats); } + /** + * Sets the minimum value. The given value must be less than or equal to 0. + * @param minValue the value + */ protected void setMinValue(BigInteger minValue) { + + if (minValue != null) { + if (minValue.compareTo(BigInteger.ZERO) > 0) { + throw new IllegalArgumentException("Min value must be <= 0"); + } + } + BigInteger value = getValue(); this.minValue = minValue; setValue(value); } + /** + * Sets the maximum value. The given value must be greater than 0. + * @param maxValue the value + */ protected void setMaxValue(BigInteger maxValue) { + + if (maxValue != null) { + if (maxValue.compareTo(BigInteger.ZERO) <= 0) { + throw new IllegalArgumentException("Max value must be > 0"); + } + } + BigInteger value = getValue(); this.maxValue = maxValue; setValue(value); @@ -381,6 +439,9 @@ public class AbstractIntegerTextField { } protected boolean isInBounds(BigInteger value) { + if (value == null) { + return false; + } if (minValue != null && minValue.compareTo(value) > 0) { return false; } @@ -391,21 +452,41 @@ public class AbstractIntegerTextField { if (text.equals("0") || text.equals("-0")) { return BigInteger.ZERO; } + String prefix = format.getPrefix(); - if (usePrefix && !prefix.isBlank()) { - if (text.startsWith(prefix)) { - text = text.substring(prefix.length()); - } - else if (text.startsWith("-" + prefix)) { - text = "-" + text.substring(prefix.length() + 1); - } - else { - return null; - } + if (prefix.isBlank()) { + return format.parse(text); } + + // auto-switching requires a prefix so we know when we should switch formats + boolean requiresPrefix = autoSwitch; + boolean hasPrefix = hasPrefix(text, prefix); + if (requiresPrefix && !hasPrefix) { + return null; + } + + text = text.replaceFirst(prefix, ""); return format.parse(text); } + private boolean hasPrefix(String text, String prefix) { + return text.startsWith(prefix) || text.startsWith("-" + prefix); + } + + private boolean hasFormatPrefix(String text, IntegerFormat format) { + if (text.startsWith("-")) { + if (!allowsNegative()) { + return false; + } + if (text.length() == 1) { + return true; + } + text = text.substring(1); + } + + return format.getPrefix().startsWith(text); + } + private boolean isValidPrefix(String text, IntegerFormat format) { if (text.startsWith("-")) { if (!allowsNegative()) { @@ -416,12 +497,13 @@ public class AbstractIntegerTextField { } text = text.substring(1); } - if (!usePrefix) { - return false; - } - return usePrefix && format.getPrefix().startsWith(text); + return format.getPrefix().startsWith(text); } +//================================================================================================= +// Inner Classes +//================================================================================================= + /** * DocumentFilter that prevents users from entering invalid data into the field. */ @@ -473,8 +555,9 @@ public class AbstractIntegerTextField { if (text.isEmpty()) { return true; } + if (isValidPrefix(text, currentFormat)) { - return true; + return true; // just a prefix and is valid } BigInteger value = parse(text, currentFormat); @@ -482,8 +565,8 @@ public class AbstractIntegerTextField { return isInBounds(value); } - if (usePrefix) { - // only allow auto switching if using number prefix + // only allow auto switching if using number prefix + if (autoSwitch) { return autoSwitchFormat(text); } return false; @@ -491,11 +574,12 @@ public class AbstractIntegerTextField { private boolean autoSwitchFormat(String text) { for (IntegerFormat format : allFormats) { - if (isValidPrefix(text, format)) { + if (hasFormatPrefix(text, format)) { currentFormat = format; textField.setFormat(format); return true; } + BigInteger value = parse(text, format); if (value != null && isInBounds(value)) { currentFormat = format; @@ -515,4 +599,30 @@ public class AbstractIntegerTextField { } } + private class CustomHintMultiFormatTextField extends MultiFormatTextField { + + public CustomHintMultiFormatTextField(int columns) { + super(columns, allFormats, m -> AbstractIntegerTextField.this.setFormat(m)); + } + + @Override + protected String getHintToolTipText() { + String superText = super.getHintToolTipText(); + if (autoSwitch) { + return superText; + } + + return superText + "

    *Auto-switch mode disabled"; + } + + @Override + protected String getHint() { + String superHint = super.getHint(); + if (autoSwitch) { + return superHint; + } + + return superHint + "*"; + } + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/MultiFormatTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/MultiFormatTextField.java index fbd6a27083..cd8b68693e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/MultiFormatTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/MultiFormatTextField.java @@ -39,17 +39,18 @@ import utility.function.Callback; public class MultiFormatTextField extends JTextField { private static final String FONT_ID = "font.input.hint"; - private int hintWidth; + private boolean showFormatHint = true; private List formats; private int currentFormatIndex; + private String toolTipAppendix; + public MultiFormatTextField(int columns, List formats, Consumer formatChangeConsumer) { super(columns); this.formats = formats; - updateFormatNameWidth(); addKeyListener(new KeyAdapter() { @Override @@ -58,7 +59,7 @@ public class MultiFormatTextField extends JTextField { if (++currentFormatIndex >= formats.size()) { currentFormatIndex = 0; } - updateFormatNameWidth(); + formatChangeConsumer.accept(formats.get(currentFormatIndex)); repaint(); } @@ -67,7 +68,6 @@ public class MultiFormatTextField extends JTextField { // make sure tooltips will be activated ToolTipManager.sharedInstance().registerComponent(this); - } /** @@ -96,30 +96,41 @@ public class MultiFormatTextField extends JTextField { }); } - private void updateFormatNameWidth() { - FontMetrics fontMetrics = getFontMetrics(Gui.getFont(FONT_ID)); - hintWidth = fontMetrics.stringWidth(formats.get(currentFormatIndex).getName()); + protected String getHint() { + return formats.get(currentFormatIndex).getName(); + } + public void setToolTipAppendix(String appendix) { + this.toolTipAppendix = appendix; } @Override public String getToolTipText(MouseEvent event) { - int hintStart = getBounds().width - hintWidth; + String hint = getHint(); + FontMetrics fontMetrics = getFontMetrics(Gui.getFont(FONT_ID)); + int hintWidth = fontMetrics.stringWidth(hint); + int hintStart = getBounds().width - hintWidth; if (event.getX() > hintStart && formats.size() > 1) { - String key = DockingUtils.CONTROL_KEY_NAME; - IntegerFormat format = formats.get(currentFormatIndex); - return "Enter value in %s format. Press %s-M to cycle input formats." - .formatted(format.getDescription(), key); + return getHintToolTipText(); } return super.getToolTipText(event); } + protected String getHintToolTipText() { + String key = DockingUtils.CONTROL_KEY_NAME; + IntegerFormat format = formats.get(currentFormatIndex); + String appendix = toolTipAppendix == null ? "" : toolTipAppendix; + return """ + Enter value in %s format.
    + Press %s-M to cycle input formats.%s + """.formatted(format.getDescription(), key, appendix); + } + /** - * Sets the {@link IntegerFormat} that will be used to format and parse the text in this - * field. + * Sets the {@link IntegerFormat} that will be used to format and parse the text in this field. * @param format the number format that will be used to format and parse the text in this field */ public void setFormat(IntegerFormat format) { @@ -127,7 +138,6 @@ public class MultiFormatTextField extends JTextField { if (indexOf >= 0) { currentFormatIndex = indexOf; } - updateFormatNameWidth(); repaint(); } @@ -153,6 +163,10 @@ public class MultiFormatTextField extends JTextField { g.setFont(Gui.getFont(FONT_ID)); g.setColor(Messages.HINT); + String hint = getHint(); + FontMetrics fontMetrics = getFontMetrics(Gui.getFont(FONT_ID)); + int hintWidth = fontMetrics.stringWidth(hint); + Dimension size = getSize(); Insets insets = getInsets(); int x; @@ -163,8 +177,7 @@ public class MultiFormatTextField extends JTextField { x = size.width - insets.right - hintWidth; } int y = size.height - insets.bottom - 1; - IntegerFormat format = formats.get(currentFormatIndex); - GraphicsUtils.drawString(this, g, format.getName(), x, y); + GraphicsUtils.drawString(this, g, hint, x, y); g.setFont(savedFont); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/IntValue.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/IntValue.java index 039b208a14..83b8e613d3 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/IntValue.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/IntValue.java @@ -70,7 +70,7 @@ public class IntValue extends AbstractValue { public JComponent getComponent() { if (field == null) { field = new IntegerTextField(20); - field.setUseNumberPrefix(false); + field.setAutoSwitchMode(false); field.setShowNumberMode(false); if (displayAsHex) { field.setFormat(IntegerFormat.HEX); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/LongValue.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/LongValue.java index 57a27007c3..036729d775 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/LongValue.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/LongValue.java @@ -58,7 +58,7 @@ public class LongValue extends AbstractValue { public JComponent getComponent() { if (field == null) { field = new IntegerTextField(20); - field.setUseNumberPrefix(false); + field.setAutoSwitchMode(false); field.setShowNumberMode(false); if (displayAsHex) { field.setFormat(IntegerFormat.HEX); diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/AbstractIntegerTextFieldTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/AbstractIntegerTextFieldTest.java index df155d2b7c..8374b8b0cf 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/AbstractIntegerTextFieldTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/AbstractIntegerTextFieldTest.java @@ -98,8 +98,8 @@ public abstract class AbstractIntegerTextFieldTest field.setUseNumberPrefix(b)); + protected void setAutoSwitchMode(boolean b) { + runSwing(() -> field.setAutoSwitchMode(b)); } class TestChangeListener implements ChangeListener { diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/IntegerTextFieldTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/IntegerTextFieldTest.java index 4b3e9f4d61..b24871254b 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/IntegerTextFieldTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/IntegerTextFieldTest.java @@ -80,7 +80,7 @@ public class IntegerTextFieldTest extends AbstractIntegerTextFieldTest