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 1abfa51bf9..a5b685e578 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 @@ -24,6 +24,7 @@ import javax.swing.*; import javax.swing.table.TableCellEditor; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; public class HexBigIntegerTableCellEditor extends AbstractCellEditor implements TableCellEditor { protected IntegerTextField input; @@ -48,11 +49,11 @@ public class HexBigIntegerTableCellEditor extends AbstractCellEditor implements int row, int column) { input = new IntegerTextField(); input.getComponent().setBorder(UIManager.getBorder("Table.focusCellHighlightBorder")); - input.setAllowNegativeValues(true); - input.setHexMode(); - input.setAllowsHexPrefix(false); + input.setMinValue(null); // allow negative numbers + input.setFormat(IntegerFormat.HEX); + input.setUseNumberPrefix(false); input.setShowNumberMode(true); - input.setHorizontalAlignment(JTextField.RIGHT); + input.setHorizontalAlignment(SwingConstants.RIGHT); if (value != null) { input.setValue((BigInteger) value); diff --git a/Ghidra/Extensions/MachineLearning/src/main/java/ghidra/machinelearning/functionfinding/FunctionStartRFParamsDialog.java b/Ghidra/Extensions/MachineLearning/src/main/java/ghidra/machinelearning/functionfinding/FunctionStartRFParamsDialog.java index dbba764f20..9ad48f2eb9 100644 --- a/Ghidra/Extensions/MachineLearning/src/main/java/ghidra/machinelearning/functionfinding/FunctionStartRFParamsDialog.java +++ b/Ghidra/Extensions/MachineLearning/src/main/java/ghidra/machinelearning/functionfinding/FunctionStartRFParamsDialog.java @@ -16,6 +16,7 @@ package ghidra.machinelearning.functionfinding; import java.awt.BorderLayout; +import java.math.BigInteger; import java.util.*; import java.util.stream.Collectors; import java.util.stream.LongStream; @@ -438,7 +439,7 @@ public class FunctionStartRFParamsDialog extends ReusableDialogComponentProvider minUndefRangeLabel.setToolTipText(MIN_UNDEFINED_RANGE_SIZE_TIP); funcDataPanel.add(minUndefRangeLabel); minUndefRangeField = new IntegerTextField(); - minUndefRangeField.setAllowNegativeValues(false); + minUndefRangeField.setMinValue(BigInteger.ZERO); minUndefRangeField.setValue(plugin.getMinUndefinedRangeSize()); funcDataPanel.add(minUndefRangeField.getComponent()); 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 ed17da28f2..89f1389d7f 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 @@ -127,8 +127,7 @@ public class BSimSearchDialog extends AbstractBSimSearchDialog { maxResultsField = new IntegerTextField(10); maxResultsField.setValue(100); maxResultsField.setMinValue(BigInteger.ONE); - maxResultsField.setAllowNegativeValues(false); - maxResultsField.setAllowsHexPrefix(false); + maxResultsField.setUseNumberPrefix(false); maxResultsField.setShowNumberMode(false); JComponent maxResultsComponent = maxResultsField.getComponent(); diff --git a/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/Registers.htm b/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/Registers.htm index 2935b5eec7..9957f9c981 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/Registers.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/Registers.htm @@ -71,24 +71,27 @@
-+Tool Buttons
-
-
Toggles whether or not to select the row in the +
-
Toggles whether or not to select the row in the currently selected register value table whose address range contains the current address of the cursor in the listing view. For example, in the Register Manager image show above, if the user clicks on any address between 804c12 and 804c24, then the first row of the table will be selected if this action toggle is on.
Deletes the register value associations for all the +
+ +
Brings up the Add Register Value Range dialog for setting a + value over an address range for the selected register. See the + Edit Address Range section below for dialog details as this action shares the + same dialog as the edit range action.
-
Deletes the register value associations for all the selected ranges in the table.
Creates a selection in the browser for all the address +
-
Creates a selection in the browser for all the address ranges selected in the register values table.
Filters out all registers in the register tree that +
Filters out all registers in the register tree that don't have any associated values (default or otherwise).
diff --git a/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/ClearRegisterValues.png b/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/ClearRegisterValues.png index 621122c4fd..ac97682b56 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/ClearRegisterValues.png and b/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/ClearRegisterValues.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/EditRegisterValueRange.png b/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/EditRegisterValueRange.png index fc6699904d..8d2140e41f 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/EditRegisterValueRange.png and b/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/EditRegisterValueRange.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/RegisterManager.png b/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/RegisterManager.png index 5f670f3675..cb7cf207c7 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/RegisterManager.png and b/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/RegisterManager.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/SetRegisterValues.png b/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/SetRegisterValues.png index 18b5ebfe1c..5c6bf97309 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/SetRegisterValues.png and b/Ghidra/Features/Base/src/main/help/help/topics/RegisterPlugin/images/SetRegisterValues.png differ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/AbstractSettingsDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/AbstractSettingsDialog.java index c14c093afc..f4556047c7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/AbstractSettingsDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/AbstractSettingsDialog.java @@ -31,6 +31,7 @@ import docking.widgets.combobox.GhidraComboBox; import docking.widgets.dialogs.StringChoices; import docking.widgets.table.*; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; import ghidra.docking.settings.*; import ghidra.framework.preferences.Preferences; import ghidra.util.BigEndianDataConverter; @@ -734,7 +735,8 @@ public abstract class AbstractSettingsDialog extends DialogComponentProvider { } private void updateHexMode() { - intHexModeMap.put(rowobject.definition.getName(), intTextField.isHexMode()); + intHexModeMap.put(rowobject.definition.getName(), + intTextField.getFormat() == IntegerFormat.HEX); } private Number getNumber() { @@ -805,14 +807,14 @@ public abstract class AbstractSettingsDialog extends DialogComponentProvider { mode = NUMBER; NumberSettingsDefinition def = (NumberSettingsDefinition) rowobject.definition; if (def.isHexModePreferred() || isHexModeEnabled(def)) { - intTextField.setHexMode(); + intTextField.setFormat(IntegerFormat.HEX); } else { - intTextField.setDecimalMode(); + intTextField.setFormat(IntegerFormat.DEC); } intTextField.setMaxValue(def.getMaxValue()); - intTextField.setAllowNegativeValues(def.allowNegativeValue()); + intTextField.setMinValue(def.allowNegativeValue() ? null : BigInteger.ZERO); if (value == null) { intTextField.setValue(null); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditor.java index 2bf6ccbf47..3c49d63ac9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditor.java @@ -31,6 +31,7 @@ import org.apache.commons.lang3.StringUtils; import docking.widgets.DropDownSelectionTextField; import docking.widgets.table.FocusableEditor; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; import generic.theme.GThemeDefaults.Colors.Palette; import ghidra.app.util.AddressInput; import ghidra.program.model.address.Address; @@ -163,7 +164,7 @@ class VarnodeLocationCellEditor extends AbstractCellEditor private Component createStackOffsetEditor(VarnodeInfo varnode) { offsetInput = new IntegerTextField(); - offsetInput.setHexMode(); + offsetInput.setFormat(IntegerFormat.HEX); Address address = varnode.getAddress(); if (address != null) { offsetInput.setValue(address.getOffset()); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeSizeCellEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeSizeCellEditor.java index cf52210291..9554c93f9c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeSizeCellEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeSizeCellEditor.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. @@ -24,6 +24,7 @@ import javax.swing.*; import javax.swing.table.TableCellEditor; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; import generic.theme.GThemeDefaults.Colors.Palette; class VarnodeSizeCellEditor extends AbstractCellEditor implements TableCellEditor { @@ -56,8 +57,8 @@ class VarnodeSizeCellEditor extends AbstractCellEditor implements TableCellEdito int row, int column) { input = new IntegerTextField(); - input.setAllowNegativeValues(false); - input.setDecimalMode(); + input.setMinValue(BigInteger.ZERO); + input.setFormat(IntegerFormat.DEC); Integer size = (Integer) value; if (size != null) { input.setValue(size.longValue()); 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 e08091b5ef..d1dcf36172 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 @@ -16,6 +16,7 @@ package ghidra.app.plugin.core.memory; import java.awt.*; +import java.math.BigInteger; import java.util.List; import javax.swing.*; @@ -28,6 +29,7 @@ import docking.widgets.combobox.GhidraComboBox; import docking.widgets.label.GDLabel; import docking.widgets.label.GLabel; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; import ghidra.app.plugin.core.memory.AddBlockModel.InitializedType; import ghidra.app.plugin.core.misc.RegisterField; import ghidra.app.util.AddressInput; @@ -474,16 +476,16 @@ class AddBlockDialog extends DialogComponentProvider implements ChangeListener { JPanel schemePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); schemeDestByteCountField = new IntegerTextField(4, 1); - schemeDestByteCountField.setAllowNegativeValues(false); - schemeDestByteCountField.setAllowsHexPrefix(false); - schemeDestByteCountField.setDecimalMode(); + schemeDestByteCountField.setMinValue(BigInteger.ZERO); + schemeDestByteCountField.setUseNumberPrefix(false); + schemeDestByteCountField.setFormat(IntegerFormat.DEC); schemeDestByteCountField.addChangeListener(ev -> schemeDestByteCountChanged()); schemeDestByteCountField.setAccessibleName("Mapping Ratio: Destination Size"); schemeSrcByteCountField = new IntegerTextField(4, 1); - schemeSrcByteCountField.setAllowNegativeValues(false); - schemeSrcByteCountField.setAllowsHexPrefix(false); - schemeSrcByteCountField.setDecimalMode(); + schemeSrcByteCountField.setMinValue(BigInteger.ZERO); + schemeSrcByteCountField.setUseNumberPrefix(false); + schemeSrcByteCountField.setFormat(IntegerFormat.DEC); schemeSrcByteCountField.addChangeListener(ev -> schemeSrcByteCountChanged()); schemeSrcByteCountField.setAccessibleName("Mapping Ratio: Source Size"); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/EditRegisterValueDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/EditRegisterValueDialog.java index 7794d433a7..21b9d1f733 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/EditRegisterValueDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/EditRegisterValueDialog.java @@ -22,8 +22,8 @@ import javax.swing.*; import docking.DialogComponentProvider; import docking.widgets.label.GLabel; +import docking.widgets.textfield.FixedSizeIntegerTextField; import ghidra.app.util.AddressInput; -import ghidra.app.util.bean.FixedBitSizeValueField; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Register; @@ -36,7 +36,7 @@ class EditRegisterValueDialog extends DialogComponentProvider { private AddressInput startAddrField; private AddressInput endAddrField; - private FixedBitSizeValueField registerValueField; + private FixedSizeIntegerTextField registerValueField; private boolean wasCancelled = true; EditRegisterValueDialog(Register register, Address start, Address end, BigInteger value, @@ -61,10 +61,16 @@ class EditRegisterValueDialog extends DialogComponentProvider { startAddrField = new AddressInput(program, addressChangeListener); endAddrField = new AddressInput(program, addressChangeListener); - registerValueField = new FixedBitSizeValueField(register.getBitLength(), true, false); - registerValueField.getAccessibleContext().setAccessibleName("Register Value"); - startAddrField.setAddress(start); - endAddrField.setAddress(end); + registerValueField = new FixedSizeIntegerTextField(16, register.getBitLength()); + registerValueField.getComponent() + .getAccessibleContext() + .setAccessibleName("Register Value"); + if (start != null) { + startAddrField.setAddress(start); + } + if (end != null) { + endAddrField.setAddress(end); + } registerValueField.setValue(value); JPanel panel = new JPanel(new PairLayout(5, 1)); @@ -77,7 +83,7 @@ class EditRegisterValueDialog extends DialogComponentProvider { panel.add(new GLabel("End Address:")); panel.add(endAddrField); panel.add(new GLabel("Value:")); - panel.add(registerValueField); + panel.add(registerValueField.getComponent()); panel.getAccessibleContext().setAccessibleName("Edit Register Value"); return panel; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterManagerProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterManagerProvider.java index d257767edf..965643212e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterManagerProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterManagerProvider.java @@ -19,14 +19,19 @@ import static ghidra.framework.model.DomainObjectEvent.*; import static ghidra.program.util.ProgramEvent.*; import java.awt.event.MouseEvent; +import java.math.BigInteger; import javax.swing.*; import docking.ActionContext; import docking.WindowPosition; -import docking.action.*; +import docking.action.ToggleDockingAction; +import docking.action.builder.ActionBuilder; +import docking.action.builder.ToggleActionBuilder; import generic.theme.GIcon; +import ghidra.app.cmd.register.SetRegisterCmd; import ghidra.app.context.ProgramActionContext; +import ghidra.framework.cmd.Command; import ghidra.framework.model.DomainObjectChangedEvent; import ghidra.framework.model.DomainObjectListener; import ghidra.framework.plugintool.ComponentProviderAdapter; @@ -52,16 +57,13 @@ public class RegisterManagerProvider extends ComponentProviderAdapter { private RegisterTree tree; private RegisterValuesPanel values; - private DockingAction deleteRegisterValuesAction; - - private DockingAction selectRegisterValuesAction; - private ToggleDockingAction showDefaultRegisterValuesAction; + private ToggleDockingAction showDefaultValuesAction; private DomainObjectListener domainObjectListener; private ToggleDockingAction filterRegistersAction; private SwingUpdateManager updateMgr; - private ToggleDockingAction followLocationToggleAction; - protected boolean followLocation; + private boolean followLocation = false; + private Address currentAddress; RegisterManagerProvider(PluginTool tool, String owner) { super(tool, "Register Manager", owner, ProgramActionContext.class); @@ -86,9 +88,7 @@ public class RegisterManagerProvider extends ComponentProviderAdapter { tree.addGTreeSelectionListener(e -> showRegister()); values.getTable().getSelectionModel().addListSelectionListener(e -> { - JTable table = values.getTable(); - deleteRegisterValuesAction.setEnabled(table.getSelectedRowCount() > 0); - selectRegisterValuesAction.setEnabled(table.getSelectedRowCount() > 0); + contextChanged(); }); tree.setAccessibleNamePrefix("Register Manager"); @@ -100,80 +100,91 @@ public class RegisterManagerProvider extends ComponentProviderAdapter { void createActions() { HelpLocation helpLocation = new HelpLocation("RegisterPlugin", "tool_buttons"); - deleteRegisterValuesAction = new DockingAction("Delete Register Value Ranges", getOwner()) { - @Override - public void actionPerformed(ActionContext context) { - values.deleteSelectedRanges(); - } - }; - deleteRegisterValuesAction.setEnabled(false); - deleteRegisterValuesAction.setToolBarData(new ToolBarData(DELETE_REGISTER_VALUES_ICON)); - deleteRegisterValuesAction - .setPopupMenuData(new MenuData(new String[] { "Delete Register Value Ranges" })); - deleteRegisterValuesAction.setDescription("Delete Register Value Ranges"); - deleteRegisterValuesAction.setHelpLocation(helpLocation); - tool.addLocalAction(this, deleteRegisterValuesAction); - selectRegisterValuesAction = new DockingAction("Select Register Value Ranges", getOwner()) { - @Override - public void actionPerformed(ActionContext context) { - values.selectedRanges(); - } - }; - selectRegisterValuesAction.setEnabled(false); - selectRegisterValuesAction.setToolBarData(new ToolBarData(SELECT_REGISTER_VALUES_ICON)); - selectRegisterValuesAction - .setPopupMenuData(new MenuData(new String[] { "Select Register Value Ranges" })); - selectRegisterValuesAction.setDescription("Select Register Value Ranges"); - selectRegisterValuesAction.setHelpLocation(helpLocation); - tool.addLocalAction(this, selectRegisterValuesAction); + new ActionBuilder("Add Register Value Range", getOwner()) + .toolBarIcon(Icons.ADD_ICON) + .popupMenuPath("Add Value Range") + .popupMenuIcon(Icons.ADD_ICON) + .description("Add a new register value range") + .helpLocation(helpLocation) + .withContext(RegisterManagerContext.class) + .enabledWhen(c -> c.getSelectedRegister() != null) + .onAction(c -> addRange(c.getSelectedRegister())) + .buildAndInstallLocal(this); - showDefaultRegisterValuesAction = - new ToggleDockingAction("Show default register values", getOwner()) { - @Override - public void actionPerformed(ActionContext context) { - values.setShowDefaultValues(showDefaultRegisterValuesAction.isSelected()); - } - }; - showDefaultRegisterValuesAction.setSelected(false); - showDefaultRegisterValuesAction - .setDescription("Toggles showing of default register values"); - showDefaultRegisterValuesAction - .setMenuBarData(new MenuData(new String[] { "Show Default Values" })); - showDefaultRegisterValuesAction - .setHelpLocation(new HelpLocation("RegisterPlugin", "menu_actions")); - tool.addLocalAction(this, showDefaultRegisterValuesAction); + new ActionBuilder("Delete Register Value Ranges", getOwner()) + .toolBarIcon(DELETE_REGISTER_VALUES_ICON) + .popupMenuPath("Delete Register Value Ranges") + .popupMenuIcon(DELETE_REGISTER_VALUES_ICON) + .description("Delete selected register value ranges") + .helpLocation(helpLocation) + .withContext(RegisterManagerContext.class) + .enabledWhen(c -> c.hasSelectedRegisterValueRanges()) + .onAction(c -> values.deleteSelectedRanges()) + .buildAndInstallLocal(this); - filterRegistersAction = new ToggleDockingAction("Filter Registers", getOwner()) { - @Override - public void actionPerformed(ActionContext context) { - tree.setFiltered(filterRegistersAction.isSelected()); - } - }; - filterRegistersAction.setSelected(false); - filterRegistersAction.setDescription( - "Toggles filtering out registers that don't have values or default values."); - filterRegistersAction.setToolBarData(new ToolBarData(FILTER_ICON)); - filterRegistersAction.setHelpLocation(helpLocation); - tool.addLocalAction(this, filterRegistersAction); + new ActionBuilder("Select Register Value Ranges", getOwner()) + .toolBarIcon(SELECT_REGISTER_VALUES_ICON) + .popupMenuPath("Select Register Value Ranges") + .popupMenuIcon(SELECT_REGISTER_VALUES_ICON) + .helpLocation(helpLocation) + .description("Create a program selection from the selected rows") + .withContext(RegisterManagerContext.class) + .enabledWhen(c -> c.hasSelectedRegisterValueRanges()) + .onAction(c -> values.selectRanges()) + .buildAndInstallLocal(this); - followLocationToggleAction = - new ToggleDockingAction("Follow location changes", getOwner()) { - @Override - public void actionPerformed(ActionContext context) { - followLocation = followLocationToggleAction.isSelected(); - } - }; - followLocationToggleAction.setEnabled(true); - followLocationToggleAction.setHelpLocation(helpLocation); - followLocationToggleAction.setToolBarData(new ToolBarData(RECV_LOCATION_ICON, "NavAction")); - tool.addLocalAction(this, followLocationToggleAction); + showDefaultValuesAction = + new ToggleActionBuilder("Show Default Register Values", getOwner()) + .menuPath("Show Default Values") + .description("Toggles showing of default register values") + .helpLocation(helpLocation) + .selected(false) + .onAction(c -> updateShowingDefaultRegisterValues()) + .buildAndInstallLocal(this); + filterRegistersAction = new ToggleActionBuilder("Filter Registers", getOwner()) + .toolBarIcon(FILTER_ICON) + .description("Toggles showing only registers with values or default values") + .helpLocation(helpLocation) + .selected(false) + .onAction(c -> tree.setFiltered(filterRegistersAction.isSelected())) + .buildAndInstallLocal(this); + + new ToggleActionBuilder("Follow location changes", getOwner()) + .toolBarIcon(RECV_LOCATION_ICON) + .toolBarGroup("NavAction") + .description( + "If selected, auto select register and value range from listing location") + .helpLocation(helpLocation) + .selected(false) + .onAction(c -> followLocation = !followLocation) + .buildAndInstallLocal(this); + } + + private void updateShowingDefaultRegisterValues() { + values.setShowDefaultValues(showDefaultValuesAction.isSelected()); + } + + private void addRange(Register register) { + EditRegisterValueDialog dialog = + new EditRegisterValueDialog(register, currentAddress, currentAddress, null, program); + dialog.setTitle("Add Value Range"); + tool.showDialog(dialog, this); + + if (!dialog.wasCancelled()) { + Address start = dialog.getStartAddress(); + Address end = dialog.getEndAddress(); + BigInteger value = dialog.getValue(); + Commandcommand = new SetRegisterCmd(register, start, end, value); + tool.execute(command, program); + } } private void showRegister() { Register register = tree.getSelectedRegister(); values.setRegister(register); + contextChanged(); } @Override @@ -191,7 +202,8 @@ public class RegisterManagerProvider extends ComponentProviderAdapter { if (program == null) { return null; } - return new ProgramActionContext(this, program); + return new RegisterManagerContext(program, tree.getSelectedRegister(), + values.hasSelectedRows()); } @Override @@ -245,6 +257,7 @@ public class RegisterManagerProvider extends ComponentProviderAdapter { } public void setLocation(Register register, Address address) { + currentAddress = address; if (!followLocation) { return; } @@ -254,4 +267,23 @@ public class RegisterManagerProvider extends ComponentProviderAdapter { values.setAddress(address); } + private class RegisterManagerContext extends ProgramActionContext { + private Register register; + private boolean hasSelectedRows; + + RegisterManagerContext(Program program, Register register, boolean hasSelectedRows) { + super(RegisterManagerProvider.this, program); + this.register = register; + this.hasSelectedRows = hasSelectedRows; + } + + public boolean hasSelectedRegisterValueRanges() { + return hasSelectedRows; + } + + public Register getSelectedRegister() { + return register; + } + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterPlugin.java index 1a4d1018da..c7e04686be 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterPlugin.java @@ -358,15 +358,13 @@ public class RegisterPlugin extends ProgramPlugin { @Override protected void locationChanged(ProgramLocation loc) { - if (loc instanceof RegisterTransitionFieldLocation) { - RegisterTransitionFieldLocation regLoc = (RegisterTransitionFieldLocation) loc; + if (loc instanceof RegisterTransitionFieldLocation regLoc) { Register reg = regLoc.getRegister(); if (reg != null) { registerMgrProvider.setLocation(reg, regLoc.getAddress()); } } - else if (loc instanceof RegisterFieldLocation) { - RegisterFieldLocation regLoc = (RegisterFieldLocation) loc; + else if (loc instanceof RegisterFieldLocation regLoc) { Register reg = regLoc.getRegister(); if (reg != null) { registerMgrProvider.setLocation(reg, regLoc.getAddress()); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterValuesPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterValuesPanel.java index b999e1d746..6ee2981f64 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterValuesPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterValuesPanel.java @@ -83,6 +83,10 @@ class RegisterValuesPanel extends JPanel { } + boolean hasSelectedRows() { + return table.getSelectedRowCount() > 0; + } + private void editRow(int row) { RegisterValueRange range = model.values.get(row); Address start = range.getStartAddress(); @@ -275,7 +279,7 @@ class RegisterValuesPanel extends JPanel { } } - void selectedRanges() { + void selectRanges() { int[] rows = table.getSelectedRows(); AddressSet set = new AddressSet(); for (int element : rows) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/SetRegisterValueDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/SetRegisterValueDialog.java index 8eb33f004a..cac24f7a0b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/SetRegisterValueDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/SetRegisterValueDialog.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,11 +15,12 @@ */ package ghidra.app.plugin.core.register; -import java.awt.*; +import java.awt.Component; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.math.BigInteger; import java.util.Arrays; +import java.util.Iterator; import javax.swing.*; import javax.swing.event.ChangeEvent; @@ -28,19 +29,21 @@ import javax.swing.event.ChangeListener; import docking.DialogComponentProvider; import docking.widgets.combobox.GComboBox; import docking.widgets.label.GLabel; +import docking.widgets.list.GListCellRenderer; +import docking.widgets.textfield.FixedSizeIntegerTextField; import generic.theme.GThemeDefaults.Ids.Fonts; import generic.theme.Gui; -import ghidra.app.util.bean.FixedBitSizeValueField; import ghidra.program.model.address.*; import ghidra.program.model.lang.Register; import ghidra.program.model.lang.RegisterValue; import ghidra.program.model.listing.Program; import ghidra.util.HelpLocation; +import ghidra.util.layout.VariableHeightPairLayout; public class SetRegisterValueDialog extends DialogComponentProvider { private JComboBox registerComboBox; - private FixedBitSizeValueField registerValueField; - private JList addressRangeList; + private FixedSizeIntegerTextField registerValueField; + private JList addressRangeList; private BigInteger registerValue; private Register selectedRegister; private boolean useValueField; @@ -58,7 +61,7 @@ public class SetRegisterValueDialog extends DialogComponentProvider { addOKButton(); addCancelButton(); if (useValueField) { - setFocusComponent(registerValueField.getTextComponent()); + setFocusComponent(registerValueField.getComponent()); } setSelectedRegister(register); setAddressRanges(addrSet); @@ -77,79 +80,72 @@ public class SetRegisterValueDialog extends DialogComponentProvider { } private JComponent buildWorkPanel(Register[] registers) { - registerComboBox = new GComboBox<>(wrapRegisters(registers)); - registerValueField = new FixedBitSizeValueField(32, true, false); + GLabel registerLabel = new GLabel("Register:"); + GLabel valueLabel = new GLabel("Value:"); + GLabel addressLabel = new GLabel("Address(es):"); + registerLabel.setHorizontalAlignment(SwingConstants.RIGHT); + valueLabel.setHorizontalAlignment(SwingConstants.RIGHT); + addressLabel.setHorizontalAlignment(SwingConstants.RIGHT); + addressLabel.setVerticalAlignment(SwingConstants.TOP); + + JPanel panel = new JPanel(new VariableHeightPairLayout(10, 10)); + panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5)); + panel.add(registerLabel); + panel.add(buildRegisterComboBox(registers)); + panel.add(valueLabel); + panel.add(buildValueField(registers)); + panel.add(addressLabel); + panel.add(buildAddressPanel()); + return panel; + } + + private Component buildValueField(Register[] registers) { + registerValueField = new FixedSizeIntegerTextField(16, 16); registerValueField.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { updateOkEnablement(); } }); + return registerValueField.getComponent(); + } + + private Component buildRegisterComboBox(Register[] registers) { + registerComboBox = new GComboBox<>(wrapRegisters(registers)); + registerComboBox.setRenderer(new RegisterComboRenderer()); registerComboBox.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { registerChanged(); + updateComboToolTip(); } }); + updateComboToolTip(); + return registerComboBox; + } - addressRangeList = new JList(); + private void updateComboToolTip() { + RegisterWrapper item = (RegisterWrapper) registerComboBox.getSelectedItem(); + String tooltip = item == null ? "" : item.getToolTip(); + registerComboBox.setToolTipText(tooltip); + } + + private Component buildAddressPanel() { + addressRangeList = new JList (); addressRangeList.setEnabled(false); Gui.registerFont(addressRangeList, Fonts.MONOSPACED); JScrollPane scrollPane = new JScrollPane(addressRangeList); scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); - Dimension d = scrollPane.getPreferredSize(); - d.height = 120; - d.width = 180; - scrollPane.setPreferredSize(d); - JPanel panel = new JPanel(new GridBagLayout()); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.anchor = GridBagConstraints.WEST; - gbc.insets = new Insets(5, 5, 1, 5); - gbc.gridx = 0; - gbc.gridy = 0; - panel.add(new GLabel("Register:"), gbc); - gbc.gridy = 1; - if (useValueField) { - panel.add(new GLabel("Value:"), gbc); - } - gbc.gridy = 2; - - gbc.anchor = GridBagConstraints.NORTHWEST; - gbc.insets = new Insets(10, 5, 1, 5); - GLabel addressLabel = new GLabel("Address(es):"); - addressLabel.setVerticalAlignment(SwingConstants.TOP); - panel.add(addressLabel, gbc); - - gbc.insets = new Insets(5, 5, 1, 5); - gbc.weightx = 1.0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.gridx = 1; - gbc.gridy = 0; - panel.add(registerComboBox, gbc); - gbc.gridy = 1; - if (useValueField) { - panel.add(registerValueField, gbc); - } - - gbc.gridy = 2; - gbc.weighty = 1.0; - gbc.fill = GridBagConstraints.BOTH; - panel.add(scrollPane, gbc); - - panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5)); - - return panel; - + return scrollPane; } private void registerChanged() { RegisterWrapper wrapper = (RegisterWrapper) registerComboBox.getSelectedItem(); if (wrapper != null) { - registerValueField.setBitSize(wrapper.register.getBitLength()); + int bitLength = wrapper.register.getBitLength(); + registerValueField.setBitSize(bitLength); updateOkEnablement(); } updateValue(); @@ -232,6 +228,21 @@ public class SetRegisterValueDialog extends DialogComponentProvider { return selectedRegister; } + private static class RegisterComboRenderer extends GListCellRenderer { + @Override + public Component getListCellRendererComponent(JList extends RegisterWrapper> list, + RegisterWrapper value, int index, boolean isSelected, boolean hasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, hasFocus); + String toolTip = value.getToolTip(); + setToolTipText(toolTip); + return this; + } + + @Override + protected String getItemText(RegisterWrapper value) { + return value == null ? "" : value.toString(); + } + } } class RegisterWrapper implements Comparable { @@ -240,14 +251,24 @@ class RegisterWrapper implements Comparable { RegisterWrapper(Register register) { this.register = register; - displayName = register.getName() + " (" + register.getBitLength() + getAliases() + ")"; + displayName = register.getName() + " (" + register.getBitLength() + ")"; } - private String getAliases() { + String getToolTip() { StringBuffer buf = new StringBuffer(); - for (String alias : register.getAliases()) { - buf.append(buf.length() == 0 ? "; " : ", "); - buf.append(alias); + buf.append(displayName); + buf.append(" Aliases: "); + Iterator aliases = register.getAliases().iterator(); + if (!aliases.hasNext()) { + buf.append("none"); + } + else { + buf.append(aliases.next()); + buf.append(", "); + } + while (aliases.hasNext()) { + buf.append(", "); + buf.append(aliases.next()); } return buf.toString(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/select/SelectBlockDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/select/SelectBlockDialog.java index 947a6877e5..c42d876112 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/select/SelectBlockDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/select/SelectBlockDialog.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. @@ -97,7 +97,7 @@ class SelectBlockDialog extends ReusableDialogComponentProvider { numberInputField = new IntegerTextField(10); numberInputField.getComponent().getAccessibleContext().setAccessibleName("Number Input"); numberInputField.setMaxValue(BigInteger.valueOf(Integer.MAX_VALUE)); - numberInputField.setAllowNegativeValues(false); + numberInputField.setMinValue(BigInteger.ZERO); main.add(numberInputField.getComponent()); main.getAccessibleContext().setAccessibleName("Block"); return main; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/StringTableProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/StringTableProvider.java index 0da884bf0a..54f124bf9c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/StringTableProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/StringTableProvider.java @@ -17,6 +17,7 @@ package ghidra.app.plugin.core.string; import java.awt.*; import java.awt.event.KeyEvent; +import java.math.BigInteger; import java.util.ArrayList; import java.util.List; @@ -331,7 +332,7 @@ public class StringTableProvider extends ComponentProviderAdapter implements Dom private Component buildOffsetPanel() { offsetField = new IntegerTextField(4, 0L); - offsetField.setAllowNegativeValues(false); + offsetField.setMinValue(BigInteger.ZERO); offsetField.addChangeListener(e -> updatePreview()); preview = new JTextField(5); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddressInput.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddressInput.java index 09e798ad70..9a0f7cebff 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddressInput.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddressInput.java @@ -15,22 +15,22 @@ */ package ghidra.app.util; +import static docking.widgets.textfield.integer.IntegerFormat.*; + import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.event.ActionListener; -import java.util.Arrays; -import java.util.Comparator; +import java.util.*; import java.util.function.Consumer; import java.util.function.Predicate; import javax.swing.*; import javax.swing.border.Border; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; import docking.widgets.combobox.GComboBox; import docking.widgets.table.FocusableEditor; -import docking.widgets.textfield.HexDecimalModeTextField; +import docking.widgets.textfield.integer.MultiFormatTextField; +import docking.widgets.textfield.integer.IntegerFormat; import generic.expressions.ExpressionException; import ghidra.program.model.address.*; import ghidra.program.model.listing.Program; @@ -45,7 +45,7 @@ public class AddressInput extends JPanel implements FocusableEditor { public final static Predicate ALL_MEMORY_SPACES = s -> s.isMemorySpace(); public final static Predicate LOADED_MEMORY_SPACES = s -> s.isLoadedMemorySpace(); - private HexDecimalModeTextField textField; + private MultiFormatTextField textField; private AddressSpaceField addressSpaceField; AddressEvaluator addressEvaluator; private Predicate addressSpaceFilter = LOADED_MEMORY_SPACES; @@ -170,8 +170,9 @@ public class AddressInput extends JPanel implements FocusableEditor { * @param hexMode true to assume numbers are hexadecimal. */ public void setAssumeHex(boolean hexMode) { - textField.setHexMode(hexMode); - hexModeChanged(hexMode); + IntegerFormat format = hexMode ? IntegerFormat.HEX : IntegerFormat.DEC; + textField.setFormat(format); + hexModeChanged(format); } /** @@ -418,35 +419,17 @@ public class AddressInput extends JPanel implements FocusableEditor { private void buildComponent() { setLayout(new BorderLayout()); - textField = new HexDecimalModeTextField(10, b -> hexModeChanged(b)); - textField.setHexMode(true); + textField = new MultiFormatTextField(10, List.of(HEX, DEC), b -> hexModeChanged(b)); textField.setName("JTextField");//for JUnits... addressSpaceField = new AddressSpaceField(); add(textField, BorderLayout.CENTER); comboAdded = false; - - textField.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { - notifyAddressChanged(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - notifyAddressChanged(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - notifyAddressChanged(); - } - }); - + textField.addTextChangedCallback(this::notifyAddressChanged); } - private void hexModeChanged(boolean hexMode) { - this.assumeHex = hexMode; - addressEvaluator.setAssumeHex(hexMode); + private void hexModeChanged(IntegerFormat format) { + this.assumeHex = format == HEX; + addressEvaluator.setAssumeHex(assumeHex); notifyAddressChanged(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/importer/options/HexLongOption.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/importer/options/HexLongOption.java index 6436c8bf9b..5466af8918 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/importer/options/HexLongOption.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/importer/options/HexLongOption.java @@ -18,6 +18,7 @@ package ghidra.app.util.importer.options; import java.awt.Component; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; import ghidra.app.util.*; import ghidra.app.util.opinion.Loader; import ghidra.framework.options.SaveState; @@ -66,7 +67,7 @@ public class HexLongOption extends AbstractOption { setValue(new HexLong(initialState)); IntegerTextField field = new IntegerTextField(); field.setValue(initialState); - field.setHexMode(); + field.setFormat(IntegerFormat.HEX); field.getComponent().setToolTipText(getDescription()); field.addChangeListener(e -> { setValue(new HexLong(field.getLongValue())); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/AddressFieldOptionsPropertyEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/AddressFieldOptionsPropertyEditor.java index 86858db870..f9556e63ba 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/AddressFieldOptionsPropertyEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/AddressFieldOptionsPropertyEditor.java @@ -26,6 +26,7 @@ import docking.widgets.checkbox.GCheckBox; import docking.widgets.combobox.GhidraComboBox; import docking.widgets.label.GDLabel; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; import ghidra.framework.options.CustomOptionsEditor; import ghidra.util.HTMLUtilities; import ghidra.util.layout.PairLayout; @@ -101,8 +102,8 @@ public class AddressFieldOptionsPropertyEditor extends PropertyEditorSupport panel.add(label); minDigitsField = new IntegerTextField(2); - minDigitsField.setAllowNegativeValues(false); - minDigitsField.setDecimalMode(); + minDigitsField.setMinValue(BigInteger.ZERO); + minDigitsField.setFormat(IntegerFormat.DEC); minDigitsField.setMaxValue(BigInteger.valueOf(32)); minDigitsField.getComponent().setToolTipText(MIN_HEX_DIGITS_TOOLTIP); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ArrayElementPropertyEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ArrayElementPropertyEditor.java index e3c636dcf9..59947d141d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ArrayElementPropertyEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ArrayElementPropertyEditor.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. @@ -20,6 +20,7 @@ import java.awt.Container; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.beans.PropertyEditorSupport; +import java.math.BigInteger; import javax.swing.*; import javax.swing.border.TitledBorder; @@ -97,7 +98,7 @@ public class ArrayElementPropertyEditor extends PropertyEditorSupport Container parent) { IntegerTextField textField = new IntegerTextField(10); - textField.setAllowNegativeValues(false); + textField.setMinValue(BigInteger.ZERO); textField.setEnabled(true); JPanel textFieldPanel = new JPanel(); @@ -132,7 +133,8 @@ public class ArrayElementPropertyEditor extends PropertyEditorSupport } private void setLocalValues(ArrayElementWrappedOption namespaceOption) { - if (namespaceOption.showMultipleArrayElementPerLine() != groupElementsCheckBox.isSelected()) { + if (namespaceOption.showMultipleArrayElementPerLine() != groupElementsCheckBox + .isSelected()) { groupElementsCheckBox.setSelected(namespaceOption.showMultipleArrayElementPerLine()); } if (namespaceOption.getArrayElementsPerLine() != elementsPerLineField.getIntValue()) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/ProgramByteSource.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/ProgramByteSource.java index 39d44fa82d..4e8715f151 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/ProgramByteSource.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/ProgramByteSource.java @@ -64,7 +64,7 @@ public class ProgramByteSource implements AddressableByteSource { long offset = location.getByteAddress().subtract(location.getProgram().getImageBase()); return getProgram().getImageBase().add(offset); } - + public Program getProgram() { return memory.getProgram(); } 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 78b5ea4b00..71357bccec 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 @@ -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. @@ -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.IntegerFormat; import ghidra.util.Swing; /** @@ -109,10 +110,10 @@ public abstract class AbstractNumberInputDialog extends DialogComponentProvider }); if (showAsHex) { - numberInputField.setHexMode(); + numberInputField.setFormat(IntegerFormat.HEX); } if (min.compareTo(BigInteger.valueOf(0)) >= 0) { - numberInputField.setAllowNegativeValues(false); + numberInputField.setMinValue(BigInteger.ZERO); } return panel; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/IntegerRangeConstraintEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/IntegerRangeConstraintEditor.java index 7a326a4d46..41d245aa29 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/IntegerRangeConstraintEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/IntegerRangeConstraintEditor.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,6 +15,8 @@ */ package docking.widgets.table.constrainteditor; +import static docking.widgets.textfield.integer.IntegerFormat.*; + import java.awt.Component; import java.awt.GridLayout; import java.math.BigInteger; @@ -129,7 +131,8 @@ public class IntegerRangeConstraintEditor BigInteger delta = bigEnd.subtract(bigStart).add(BigInteger.ONE); boolean hexMode = - lowerSpinner.getTextField().isHexMode() || upperSpinner.getTextField().isHexMode(); + lowerSpinner.getTextField().getFormat() == HEX || + upperSpinner.getTextField().getFormat() == HEX; String statusMsg = formatStatus( String.format("Range Size: " + (hexMode ? "0x%x" : "%,d"), delta), false); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/UnsignedLongConstraintEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/UnsignedLongConstraintEditor.java index be71a17fcd..06a58ee941 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/UnsignedLongConstraintEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/UnsignedLongConstraintEditor.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. @@ -25,6 +25,7 @@ import docking.widgets.label.GDHtmlLabel; import docking.widgets.table.constraint.ColumnConstraint; import docking.widgets.table.constraint.SingleValueColumnConstraint; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; import generic.theme.GThemeDefaults.Colors.Messages; /** @@ -54,8 +55,8 @@ public class UnsignedLongConstraintEditor extends AbstractColumnConstraintEditor getConstraint().getConstraintValue(); field = new IntegerTextField(16, 0); - field.setHexMode(); - field.setAllowNegativeValues(false); + field.setFormat(IntegerFormat.HEX); + field.setMinValue(BigInteger.ZERO); // don't allow negative numbers field.setMaxValue(new BigInteger("FFFFFFFFFFFFFFFF", 16)); field.addChangeListener(e -> valueChanged()); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/UnsignedLongRangeConstraintEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/UnsignedLongRangeConstraintEditor.java index 7db6f44e83..f6d1f4ed40 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/UnsignedLongRangeConstraintEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/UnsignedLongRangeConstraintEditor.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. @@ -25,6 +25,7 @@ import docking.widgets.label.GDHtmlLabel; import docking.widgets.table.constraint.ColumnConstraint; import docking.widgets.table.constraint.RangeColumnConstraint; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; import generic.theme.GThemeDefaults.Colors.Messages; import ghidra.util.layout.VerticalLayout; @@ -80,8 +81,8 @@ public class UnsignedLongRangeConstraintEditor extends AbstractColumnConstraintE } private void configureField(IntegerTextField field) { - field.setHexMode(); - field.setAllowNegativeValues(false); + field.setFormat(IntegerFormat.HEX); + field.setMinValue(BigInteger.ZERO); field.setMaxValue(MAX_VALUE); field.addChangeListener(e -> valueChanged()); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/FixedSizeIntegerTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/FixedSizeIntegerTextField.java new file mode 100644 index 0000000000..358c3dafda --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/FixedSizeIntegerTextField.java @@ -0,0 +1,173 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.widgets.textfield; + +import static docking.widgets.textfield.integer.IntegerFormat.*; + +import java.math.BigInteger; + +import docking.widgets.textfield.integer.AbstractIntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; + +/** + * TextField for entering numbers where the values are restricted to those that can be represented + * by a specific number of bits. For example, if the bit size is eight, signed values + * must be between -128 and 127 and unsigned values must be between 0 and 255. By + * default, this class uses all the formats from the {@link IntegerFormat} enum class except for + * signed octal and signed binary. + * + * + * This field does continuous checking, so you can't enter a bad value. + *
+ * The bitSize can be changed on this field which will cause its min and max values to change to + * the appropriate values for that bit size and signedness of the current {@link IntegerFormat}. + * Also, if the current value doesn't fit in the new bit size, it will be reset to having no + * value (textfield is blank). + *
+ * Internally, values are maintained using BigIntegers so this class can accommodate any bit size + * desired. There are convenience methods for getting the value as either an int or long. You + * should only use these convenience methods if you know the current bit size fits in either a + * int or long respectively. + * + *
+ * There are several configuration options as follows: + *
+ *
+ * + */ +public class FixedSizeIntegerTextField extends AbstractIntegerTextField { + + private int bitSize; + private BigInteger minSignedValue; + private BigInteger minUnsignedValue; + private BigInteger maxSignedValue; + private BigInteger maxUnsignedValue; + + /** + * Constructor + * @param columns the number of character positions for the preferred size of the text field + * @param bitSize the initial bit size + */ + public FixedSizeIntegerTextField(int columns, int bitSize) { + this(columns, bitSize, null); + } + + /** + * Constructor + * @param columns the number of character positions for the preferred size of the text field + * @param bitSize the initial bit size + * @param initialValue the value to initialize the field to + */ + public FixedSizeIntegerTextField(int columns, int bitSize, long initialValue) { + this(columns, bitSize, BigInteger.valueOf(initialValue)); + } + + /** + * Constructor + * @param columns the number of character positions for the preferred size of the text field + * @param bitSize the initial bit size + * @param initialValue the value to initialize the field to + */ + public FixedSizeIntegerTextField(int columns, int bitSize, BigInteger initialValue) { + super(columns, initialValue, DEC, U_DEC, HEX, U_HEX, U_OCT, U_BIN); + setBitSize(bitSize); + } + + /** + * Sets the bit size for this field, which effectively sets the min and max values for this + * field when combined with the signedness of the currently selected {@link IntegerFormat}. + * @param bitSize the number of bits that will be used to store this value, effectively + * determining its min and max value + */ + public void setBitSize(int bitSize) { + if (bitSize < 1) { + throw new IllegalArgumentException("Bit size must be greater than 0"); + } + minUnsignedValue = BigInteger.ZERO; + maxUnsignedValue = BigInteger.TWO.pow(bitSize).subtract(BigInteger.ONE); + minSignedValue = BigInteger.TWO.pow(bitSize - 1).negate(); + maxSignedValue = BigInteger.TWO.pow(bitSize - 1).subtract(BigInteger.ONE); + + BigInteger value = getValue(); + this.bitSize = bitSize; + updateMinMax(); + setValue(value); + } + + /** + * {@return the current bit size for this field} + */ + public int getBitSize() { + return bitSize; + } + + @Override + public void setValue(BigInteger newValue) { + if (newValue != null && !isInBounds(newValue)) { + if (currentFormat.isUnsigned()) { + newValue = maybeConvertToUnsigned(newValue); + } + else { + newValue = maybeConvertToSigned(newValue); + } + } + super.setValue(newValue); + } + + private BigInteger maybeConvertToUnsigned(BigInteger value) { + // conversion only makes sense if value is a negative number in the signed range + if (value.compareTo(minSignedValue) >= 0 && value.compareTo(BigInteger.ZERO) < 0) { + return value.add(BigInteger.TWO.pow(bitSize)); + } + return value; + } + + private BigInteger maybeConvertToSigned(BigInteger value) { + // conversion only makes sense if value is a positive number in the unsigned range + if (value.compareTo(BigInteger.ZERO) > 0 && value.compareTo(maxUnsignedValue) <= 0) { + return value.subtract(BigInteger.TWO.pow(bitSize)); + } + return value; + } + + private void updateMinMax() { + if (currentFormat.isUnsigned()) { + setMinValue(minUnsignedValue); + setMaxValue(maxUnsignedValue); + } + else { + setMinValue(minSignedValue); + setMaxValue(maxSignedValue); + } + } + + @Override + public void setFormat(IntegerFormat format) { + BigInteger value = getValue(); + super.setFormat(format); + updateMinMax(); + setValue(value); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/HexDecimalModeTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/HexDecimalModeTextField.java deleted file mode 100644 index 02a1a88169..0000000000 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/HexDecimalModeTextField.java +++ /dev/null @@ -1,114 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package docking.widgets.textfield; - -import java.awt.*; -import java.awt.event.*; -import java.util.function.Consumer; - -import javax.swing.JTextField; -import javax.swing.ToolTipManager; - -import docking.DockingUtils; -import docking.util.GraphicsUtils; -import generic.theme.GThemeDefaults.Colors.Messages; -import generic.theme.Gui; - -/** - * Overrides the JTextField mainly to allow hint painting for the current radix mode. - */ -public class HexDecimalModeTextField extends JTextField { - - private static final String FONT_ID = "font.input.hint"; - private int hintWidth; - private boolean isHexMode; - private boolean showNumbericDecoration = true; - - public HexDecimalModeTextField(int columns, Consumer- Bit Size - This value must be 1 or greater and determines the minimum and maximum allowed + * input values when combined with the current format signedness.
+ *- 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 + * switch formats based on the prefix (or lack thereof). When the "use prefix" option is off, the + * only way to switch formats is to use the built-in 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)}
+ *modeConsumer) { - super(columns); - - FontMetrics fontMetrics = getFontMetrics(Gui.getFont(FONT_ID)); - String mode = isHexMode ? "Hex" : "Dec"; - hintWidth = fontMetrics.stringWidth(mode); - - addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_M && DockingUtils.isControlModifier(e)) { - isHexMode = !isHexMode; - modeConsumer.accept(isHexMode); - repaint(); - } - } - }); - - // make sure tooltips will be activated - ToolTipManager.sharedInstance().registerComponent(this); - } - - @Override - public String getToolTipText(MouseEvent event) { - - int hintStart = getBounds().width - hintWidth; - if (event.getX() > hintStart) { - String key = DockingUtils.CONTROL_KEY_NAME; - return "Press '" + key + "-M' to toggle Hex or Decimal Mode"; - } - - return super.getToolTipText(); - } - - public void setHexMode(boolean hexMode) { - this.isHexMode = hexMode; - } - - /** - * Turns on or off the faded text that displays the field's radix mode (hex or decimal). - * - * @param show true to show the radix mode. - */ - public void setShowNumberMode(boolean show) { - this.showNumbericDecoration = show; - repaint(); - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - if (!showNumbericDecoration) { - return; - } - - Font savedFont = g.getFont(); - g.setFont(Gui.getFont(FONT_ID)); - g.setColor(Messages.HINT); - - Dimension size = getSize(); - Insets insets = getInsets(); - int x; - if (getHorizontalAlignment() == RIGHT) { - x = insets.left; - } - else { - x = size.width - insets.right - hintWidth; - } - int y = size.height - insets.bottom - 1; - String mode = isHexMode ? "Hex" : "Dec"; - GraphicsUtils.drawString(this, g, mode, x, y); - g.setFont(savedFont); - } - -} 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 25deb873be..d81c3feb7e 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 @@ -15,20 +15,17 @@ */ package docking.widgets.textfield; -import java.awt.event.ActionListener; +import static docking.widgets.textfield.integer.IntegerFormat.*; + import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; -import javax.swing.JComponent; -import javax.swing.JTextField; -import javax.swing.event.*; -import javax.swing.text.*; - -import ghidra.util.SystemUtilities; +import docking.widgets.textfield.integer.AbstractIntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; /** - * TextField for entering integer numbers, either in decimal or hex. + * TextField for entering integer numbers in one of several number formats. By default, this class + * uses the {@link IntegerFormat#DEC} and the {@link IntegerFormat#HEX}, but it can be constructed + * with any of the formats defined in {@link IntegerFormat}. * * * This field does continuous checking, so you can't enter a bad value. @@ -43,229 +40,51 @@ import ghidra.util.SystemUtilities; *
* There are several configuration options as follows: *
- *
* */ -public class IntegerTextField { - private HexDecimalModeTextField textField; +public class IntegerTextField extends AbstractIntegerTextField { + private IntegerFormat hex; + private IntegerFormat decimal; - private boolean isHexMode = false; - private boolean allowsNegative = true; - private boolean allowsHexPrefix = true; - private BigInteger maxValue; - private BigInteger minValue; - - private List- Allows negative numbers - either support all integer numbers or just non-negative numbers. - * See {@link #setAllowNegativeValues(boolean)}
- *- Allows hex prefix - If this mode is on, then hex mode is turned on and off automatically - * depending whether or not the text starts with 0x. Otherwise, the hex/decimal mode is set - * externally (either programmatically or pressing <CTRL> M) and the user is restricted to the - * numbers/letters appropriate for that mode. See {@link #setAllowsHexPrefix(boolean)}
- *- Have a max value - a max value can be set (must be positive) such that the user can not type - * a number whose absolute value is greater than the max. Otherwise, the value is unlimited if max - * is null/unspecified. See {@link #setMaxValue(BigInteger)}
- *- Show the number mode as hint text - If on either "Hex" or "Dec" is displayed lightly in the - * bottom right portion of the text field. See {@link #setShowNumberMode(boolean)}
+ *- Max value - This value must be positive and will restrict the input to values less than or + * 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 + * 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)}
*listeners = new ArrayList<>(); - - /** - * Creates a new IntegerTextField with 5 columns and no initial value - */ public IntegerTextField() { this(5, null); } - /** - * Creates a new IntegerTextField with the specified number of columns and no initial value - * - * @param columns the number of columns. - */ public IntegerTextField(int columns) { this(columns, null); } - /** - * Creates a new IntegerTextField with the specified number of columns and an initial value - * - * @param columns the number of columns to display in the JTextField. - * @param initialValue the initial value. This constructor takes an initialValue as a long. If - * you need a value that is bigger (or smaller) than can be specified as a long, then - * use the constructor that takes a BigInteger as an initial value. - */ public IntegerTextField(int columns, long initialValue) { this(columns, BigInteger.valueOf(initialValue)); } - /** - * Creates a new IntegerTextField with the specified number of columns and initial value - * - * @param columns the number of columns - * @param initialValue the initial value - */ public IntegerTextField(int columns, BigInteger initialValue) { - textField = new HexDecimalModeTextField(columns, b -> textFieldHexModeChanged(b)); - - AbstractDocument document = (AbstractDocument) textField.getDocument(); - document.setDocumentFilter(new HexDecimalDocumentFilter()); - setValue(initialValue); - - document.addDocumentListener(new DocumentListener() { - - @Override - public void removeUpdate(DocumentEvent e) { - SystemUtilities.runSwingLater(() -> valueChanged()); - } - - @Override - public void insertUpdate(DocumentEvent e) { - SystemUtilities.runSwingLater(() -> valueChanged()); - } - - @Override - public void changedUpdate(DocumentEvent e) { - SystemUtilities.runSwingLater(() -> valueChanged()); - } - }); + super(columns, initialValue, DEC, HEX); + decimal = allFormats.get(0); + hex = allFormats.get(1); } - /** - * Adds a change listener that will be notified whenever the value changes. - * - * @param listener the change listener to add. - */ - public void addChangeListener(ChangeListener listener) { - listeners.add(listener); + @Override + public void setMaxValue(BigInteger maxValue) { + super.setMaxValue(maxValue); } - /** - * 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. - * - * @param listener the listener to be removed. - */ - public void removeChangeListener(ChangeListener listener) { - listeners.remove(listener); - } - - /** - * Returns the current value of the field or null if the field has no current value. - * - * @return the current value of the field or null if the field has no current value. - */ - public BigInteger getValue() { - String text = textField.getText(); - return computeValueFromString(text, isHexMode); - } - - /** - * Returns the current value as an int. - * - * - * If the field has no current value, 0 will be returned. If the value is bigger (or smaller) - * than an int, it will be cast to an int. - * - *
- * If using this method, it is highly recommended that you set the max value to - * {@link Integer#MAX_VALUE} or lower. - * - * @return the current value as an int. Or 0 if there is no value - * @throws ArithmeticException if the value in this field will not fit into an int - */ - public int getIntValue() { - BigInteger currentValue = getValue(); - if (currentValue == null) { - return 0; - } - return currentValue.intValueExact(); - } - - /** - * Returns the current value as a long. - * - *
- * If the field has no current value, 0 will be returned. If the value is bigger (or smaller) - * than an long, it will be cast to a long. - * - *
- * If using this method, it is highly recommended that you set the max value to - * {@link Long#MAX_VALUE} or lower. - * - * @return the current value as a long. Or 0 if there is no value - * @throws ArithmeticException if the value in this field will not fit into a long - */ - public long getLongValue() { - BigInteger currentValue = getValue(); - if (currentValue == null) { - return 0; - } - return currentValue.longValueExact(); - } - - /** - * Convenience method for setting the value to a long value; - * - * @param newValue the new value for the field. - */ - public void setValue(long newValue) { - setValue(BigInteger.valueOf(newValue)); - } - - /** - * Convenience method for setting the value to an int value; - * - * @param newValue the new value for the field. - */ - public void setValue(int newValue) { - setValue(BigInteger.valueOf(newValue)); - } - - /** - * Sets the field to the given text. The text must be a properly formated string that is a value - * that is valid for this field. If the field is set to not allow "0x" prefixes, then the input - * string cannot start with 0x and furthermore, if the field is in decimal mode, then input - * string cannot take in hex digits a-f. On the other hand, if "0x" prefixes are allowed, then - * the input string can be either a decimal number or a hex number depending on if the input - * string starts with "0x". In this case, the field's hex mode will be set to match the input - * text. If the text is not valid, the field will not change. - * - * @param text the value as text to set on this field - * @return true if the set was successful - */ - public boolean setText(String text) { - String oldText = textField.getText(); - textField.setText(text); - return !oldText.equals(textField.getText()); - } - - /** - * Sets the value of the field to the given value. A null value will clear the field. - * - * @param newValue the new value or null. - */ - public void setValue(BigInteger newValue) { - - if (!allowsNegative && newValue != null && newValue.signum() < 0) { - newValue = null; - } - - updateTextField(newValue); - } - - /** - * Turns on or off the faded text that displays the field's radix mode (hex or decimal). - * - * @param show true to show the radix mode. - */ - public void setShowNumberMode(boolean show) { - textField.setShowNumberMode(show); + @Override + public void setMinValue(BigInteger minValue) { + super.setMinValue(minValue); } /** @@ -274,17 +93,11 @@ public class IntegerTextField { *
* If the field is currently in decimal mode, the current text will be change from displaying * the current value from decimal to hex. + * @deprecated use {@link #setFormat(IntegerFormat)} instead */ + @Deprecated(forRemoval = true, since = "12.2") public void setHexMode() { - BigInteger value = getValue(); - setHexMode(true); - setValue(value); - - } - - private void setHexMode(boolean hexMode) { - this.isHexMode = hexMode; - textField.setHexMode(hexMode); + setFormat(hex); } /** @@ -293,15 +106,28 @@ public class IntegerTextField { *
* If the field is currently in hex mode, the current text will be change from displaying the * current value from hex to decimal. + * @deprecated use {@link #setFormat(IntegerFormat)} instead */ + @Deprecated(forRemoval = true, since = "12.2") public void setDecimalMode() { - BigInteger value = getValue(); - setHexMode(false); - setValue(value); + setFormat(decimal); } /** - * Sets whether on not the field supports the 0x prefix. + * Returns true if in hex mode, false if in another mode. + * + * @return true if in hex mode, false if in decimal mode. + * @deprecated use {@link #getFormat()} instead + */ + @Deprecated(forRemoval = true, since = "12.2") + public boolean isHexMode() { + return currentFormat == hex; + } + + /** + * Sets whether on not the field supports the 0x prefix for hex numbers. This method is + * deprecated since it now supports addition input modes other than hex or decimal. Turning + * the prefix on for hex will also turn it on for other non-decimal modes as well. * *
* If 0x is supported, hex numbers will be displayed with the 0x prefix. Also, when typing, you @@ -310,387 +136,29 @@ public class IntegerTextField { * can't change the decimal/hex mode by typing 0x. The field will either be in decimal or hex * mode and the typed text will be interpreted appropriately for the mode. * - * @param allowsHexPrefix true to use the 0x convention for hex. + * @param usePrefix true to use the 0x convention for hex. + * @deprecated use {@link #setUseNumberPrefix(boolean)} instead */ - public void setAllowsHexPrefix(boolean allowsHexPrefix) { - BigInteger currentValue = getValue(); - this.allowsHexPrefix = allowsHexPrefix; - updateTextField(currentValue); - } - - /** - * Returns the current text displayed in the field. - * - * @return the current text displayed in the field. - */ - public String getText() { - return textField.getText(); - } - - /** - * Returns true if in hex mode, false if in decimal mode. - * - * @return true if in hex mode, false if in decimal mode. - */ - public boolean isHexMode() { - return isHexMode; + @Deprecated(forRemoval = true, since = "12.2") + public void setAllowsHexPrefix(boolean usePrefix) { + setUseNumberPrefix(usePrefix); } /** * Sets whether or not negative numbers are accepted. * * @param b if true, negative numbers are allowed. + * @deprecated use {@link #setMinValue(BigInteger)} instead */ + @Deprecated(forRemoval = true, since = "12.2") public void setAllowNegativeValues(boolean b) { BigInteger currentValue = getValue(); - allowsNegative = b; - if (!allowsNegative) { + setMinValue(b ? null : BigInteger.ZERO); + if (!b) { if (currentValue != null && currentValue.signum() < 0) { currentValue = null; } } - updateTextField(currentValue); - } - - /** - * Returns the current maximum allowed value. Null indicates that there is no maximum value. If - * negative values are permitted (see {@link #setAllowNegativeValues(boolean)}) this value will - * establish the upper and lower limit of the absolute value. - * - * @return the current maximum value allowed. - */ - public BigInteger getMaxValue() { - return maxValue; - } - - /** - * Sets the maximum allowed value. The maximum must be a positive number. Null indicates that - * there is no maximum value. - *
- * If negative values are permitted (see {@link #setAllowNegativeValues(boolean)}) this value - * will establish the upper and lower limit of the absolute value. - * - * @param maxValue the maximum value to allow. - */ - public void setMaxValue(BigInteger maxValue) { - if (maxValue != null && maxValue.signum() < 0) { - throw new IllegalArgumentException("Max value must be positive"); - } - BigInteger currentValue = getValue(); - this.maxValue = maxValue; - if (maxValue != null && !passesMaxCheck(currentValue)) { - if (currentValue.signum() < 0) { - setValue(maxValue.negate()); - } - else { - setValue(maxValue); - } - } - } - - /** - * Sets the minimum allowed value. The minimum must be a positive number. Null indicates that - * there is no minimum value. - *
- * If negative values are permitted (see {@link #setAllowNegativeValues(boolean)}) this value - * will establish the minimum limit of the absolute value. - * - * @param minValue the minimum value to allow. - */ - public void setMinValue(BigInteger minValue) { - if (minValue != null && minValue.signum() < 0) { - throw new IllegalArgumentException("Min value must be positive"); - } - BigInteger currentValue = getValue(); - this.minValue = minValue; - if (minValue != null && !passesMinCheck(currentValue)) { - if (currentValue.signum() < 0) { - setValue(minValue.negate()); - } - else { - setValue(minValue); - } - } - } - - /** - * Returns the JTextField component that this class manages. - * - * @return the JTextField component that this class manages. - */ - public JComponent getComponent() { - return textField; - } - - /** - * Adds an ActionListener to the TextField. - * - * @param listener the ActionListener to add. - */ - public void addActionListener(ActionListener listener) { - textField.addActionListener(listener); - } - - /** - * Removes an ActionListener from the TextField. - * - * @param listener the ActionListener to remove. - */ - public void removeActionListener(ActionListener listener) { - textField.removeActionListener(listener); - } - - /** - * Sets the enablement on the JTextField component; - * - * @param enabled true for enabled, false for disabled. - */ - public void setEnabled(boolean enabled) { - textField.setEnabled(enabled); - } - - /** - * Sets the editable mode for the JTextField component - * - * @param editable boolean flag, if true component is editable - */ - public void setEditable(boolean editable) { - textField.setEditable(editable); - } - - /** - * Requests focus to the JTextField - */ - public void requestFocus() { - textField.requestFocus(); - } - - /** - * Selects the text in the JTextField - */ - public void selectAll() { - textField.selectAll(); - } - - /** - * Sets the horizontal alignment of the JTextField - * - * @param alignment the alignment as in {@link JTextField#setHorizontalAlignment(int)} - */ - public void setHorizontalAlignment(int alignment) { - textField.setHorizontalAlignment(alignment); - } - - private void textFieldHexModeChanged(boolean hexMode) { - BigInteger value = getValue(); - this.isHexMode = hexMode; - setValue(value); - } - - private String computeTextForValue(BigInteger value) { - if (value == null) { - return ""; - } - if (isHexMode) { - String text = value.toString(16); - if (allowsHexPrefix) { - if (text.startsWith("-")) { - return "-0x" + text.substring(1); - } - return "0x" + text; - } - return text; - } - - // otherwise, show as decimal - return value.toString(10); - } - - private BigInteger computeValueFromString(String text, boolean parseAsHex) { - if (text.isEmpty() || isValidPrefix(text)) { - return null; - } - - if (!parseAsHex) { - return new BigInteger(text, 10); - } - - if (allowsHexPrefix) { - if (text.startsWith("0x")) { - text = text.substring(2); - } - else if (text.startsWith("-0x")) { - text = "-" + text.substring(3); - } - } - if (text.isEmpty()) { - return null; - } - return new BigInteger(text, 16); - } - - private void valueChanged() { - for (ChangeListener listener : listeners) { - listener.stateChanged(new ChangeEvent(this)); - } - } - - private boolean passesMaxCheck(BigInteger value) { - if (value == null) { - return true; - } - if (maxValue == null) { - return true; - } - return value.abs().compareTo(maxValue) <= 0; - } - - private boolean passesMinCheck(BigInteger value) { - if (value == null) { - return true; - } - if (minValue == null) { - return true; - } - return value.abs().compareTo(minValue) >= 0; - } - - private boolean shouldParseAsHex(String text) { - if (allowsHexPrefix) { - // if allowing "0x" prefix, let the incoming text determine if we should parse as hex - return text.startsWith("0x") || text.startsWith("-0x"); - } - // otherwise parse the input string is whatever mode this field has been set to. - return isHexMode; - } - - /** - * Sets the textField to the given value taking into account the current configuration. - * - * @param value the value to convert to a string for the textField. - */ - private void updateTextField(BigInteger value) { - String text = computeTextForValue(value); - textField.setText(text); - } - - private boolean isValidPrefix(String s) { - switch (s) { - case "0x": - return allowsHexPrefix; - case "-0x": - return allowsHexPrefix && allowsNegative; - case "-": - return allowsNegative; - default: - return false; - } - - } - - /** - * DocumentFilter that prevents users from entering invalid data into the field. - */ - private class HexDecimalDocumentFilter extends DocumentFilter { - @Override - public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr) - throws BadLocationException { - - // form a string that is the current document text with the inserted new text - text = text.replace('X', 'x'); - StringBuilder builder = getText(fb); - builder.insert(offset, text); - - // if the newly formed text is valid, allow the operation - if (isValid(builder)) { - super.insertString(fb, offset, text, attr); - } - } - - @Override - public void replace(FilterBypass fb, int offset, int length, String text, - AttributeSet attrs) throws BadLocationException { - - // form a string that is the current document text with the replaced text - text = text.replace('X', 'x'); - StringBuilder builder = getText(fb); - builder.replace(offset, offset + length, text); - - // if the newly formed text is valid, allow the operation - if (isValid(builder)) { - super.replace(fb, offset, length, text, attrs); - } - } - - @Override - public void remove(FilterBypass fb, int offset, int length) throws BadLocationException { - - // form a string that is the current document text with the indicated part deleted - StringBuilder builder = getText(fb); - builder.delete(offset, offset + length); - - // if the new formed text is valid, allow the operation. - if (isValid(builder)) { - super.remove(fb, offset, length); - } - } - - private boolean isValid(StringBuilder builder) { - String valueString = builder.toString(); - - // Depending on configuration and input string, determine if we should parse as hex. - // If we don't allow "0x" prefix, then use the current hex/integer mode, Otherwise, - // parse as hex depending on whether or not the input string starts with the - // "0x" prefix. - boolean parseAsHex = shouldParseAsHex(valueString); - - // allow the string if it is the beginning of a valid string even though - // it doesn't evaluate to a number yet. - if (isValidPrefix(valueString)) { - // When the input is valid, update the hex mode to match how the text was parsed. - // See parseAsHex variable comment above. - setHexMode(parseAsHex); - return true; - } - - // otherwise, it must parse to a number to be valid. - try { - BigInteger value = computeValueFromString(valueString, parseAsHex); - if (isNonAllowedNegativeNumber(value)) { - return false; - } - if (passesMaxCheck(value) && passesMinCheck(value)) { - // When the input is valid, update the hex mode to match how the text was parsed. - // See parseAsHex variable comment above. - setHexMode(parseAsHex); - return true; - } - } - catch (NumberFormatException e) { - return false; - } - return false; - } - - // Retrieves the current document text from inside the document filter. - private StringBuilder getText(FilterBypass fb) throws BadLocationException { - StringBuilder builder = new StringBuilder(); - Document document = fb.getDocument(); - builder.append(document.getText(0, document.getLength())); - return builder; - } - - } - - private boolean isNonAllowedNegativeNumber(BigInteger value) { - if (value == null) { - return false; - } - if (allowsNegative) { - return false; - } - - // so we don't allow negatives - return value.signum() < 0; + setValue(currentValue); } } 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 new file mode 100644 index 0000000000..d6e9a7dbd2 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/AbstractIntegerTextField.java @@ -0,0 +1,515 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.widgets.textfield.integer; + +import java.awt.event.ActionListener; +import java.math.BigInteger; +import java.util.*; + +import javax.swing.JComponent; +import javax.swing.JTextField; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.text.*; + +/** + * 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 + * validation, so only valid text for the current format can be typed. + */ +public class AbstractIntegerTextField { + private MultiFormatTextField textField; + private List
listeners = new ArrayList<>(); + + protected List allFormats; + protected IntegerFormat currentFormat; + private BigInteger minValue; + private BigInteger maxValue; + private boolean usePrefix = true; + + /** + * Creates a new IntegerTextField with the specified number of columns and initial value + * + * @param columns the number of columns + * @param initialValue the initial value + * @param formats the supported InputNumberModes + */ + @SafeVarargs + public AbstractIntegerTextField(int columns, BigInteger initialValue, + IntegerFormat... formats) { + allFormats = Arrays.asList(formats); + currentFormat = allFormats.get(0); + + textField = new MultiFormatTextField(columns, allFormats, m -> setFormat(m)); + + AbstractDocument document = (AbstractDocument) textField.getDocument(); + document.setDocumentFilter(new HexDecimalDocumentFilter()); + setValue(initialValue); + textField.addTextChangedCallback(this::valueChanged); + } + + /** + * Adds a change listener that will be notified whenever the value changes. + * + * @param listener the change listener to add. + */ + public void addChangeListener(ChangeListener listener) { + 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. + * + * @param listener the listener to be removed. + */ + public void removeChangeListener(ChangeListener listener) { + listeners.remove(listener); + } + + /** + * Returns the current value of the field or null if the field has no current value. + * + * @return the current value of the field or null if the field has no current value. + */ + public BigInteger getValue() { + String text = textField.getText(); + return parse(text, currentFormat); + } + + /** + * Returns the current value as an int. + * + * + * If the field has no current value, 0 will be returned. If the value is bigger (or smaller) + * than an int, it will be cast to an int. + * + *
+ * If using this method, it is highly recommended that you set the max value to + * {@link Integer#MAX_VALUE} or lower. + * + * @return the current value as an int. Or 0 if there is no value + * @throws ArithmeticException if the value in this field will not fit into an int + */ + public int getIntValue() { + BigInteger currentValue = getValue(); + if (currentValue == null) { + if (minValue != null && minValue.equals(BigInteger.ONE)) { + return 1; + } + return 0; + } + return currentValue.intValueExact(); + } + + /** + * Returns the current value as a long. + * + *
+ * If the field has no current value, 0 will be returned. If the value is bigger (or smaller) + * than an long, it will be cast to a long. + * + *
+ * If using this method, it is highly recommended that you set the max value to + * {@link Long#MAX_VALUE} or lower. + * + * @return the current value as a long. Or 0 if there is no value + * @throws ArithmeticException if the value in this field will not fit into a long + */ + public long getLongValue() { + BigInteger currentValue = getValue(); + if (currentValue == null) { + if (minValue != null && minValue.equals(BigInteger.ONE)) { + return 1; + } + return 0; + } + return currentValue.longValueExact(); + } + + /** + * Convenience method for setting the value to a long value; + * + * @param newValue the new value for the field. + */ + public void setValue(long newValue) { + setValue(BigInteger.valueOf(newValue)); + } + + /** + * Convenience method for setting the value to an int value; + * + * @param newValue the new value for the field. + */ + public void setValue(int newValue) { + setValue(BigInteger.valueOf(newValue)); + } + + /** + * Sets the field to the given text. The text must be a properly formated string that is a valid + * value for this field. If the field is set to not allow "0x" prefixes, then the input + * string cannot start with 0x and furthermore, if the field is in decimal mode, then input + * string cannot take in hex digits a-f. On the other hand, if "0x" prefixes are allowed, then + * the input string can be either a decimal number or a hex number depending on if the input + * string starts with "0x". In this case, the field's hex mode will be set to match the input + * text. If the text is not valid, the field will not change. + * + * @param text the value as text to set on this field + * @return true if the set was successful + */ + public boolean setText(String text) { + String oldText = textField.getText(); + textField.setText(text); + return !oldText.equals(textField.getText()); + } + + /** + * Sets the value of the field to the given value. A null value will clear the field. + * + * @param newValue the new value or null. + */ + public void setValue(BigInteger newValue) { + String text = ""; + if (newValue != null && isInBounds(newValue)) { + text = currentFormat.format(newValue); + text = addPrefix(text); + } + textField.setText(text); + } + + private String addPrefix(String text) { + if (!usePrefix) { + return text; + } + String prefix = currentFormat.getPrefix(); + if (prefix.isBlank()) { + return text; + } + if (text.startsWith("-")) { + return "-" + prefix + text.substring(1); + } + return prefix + text; + } + + /** + * Turns on or off the faded text that displays the field's radix mode (hex or decimal). + * + * @param show true to show the radix mode. + */ + public void setShowNumberMode(boolean show) { + textField.setShowInputFormatHint(show); + } + + /** + * Sets the format for entering an integer into this field. The current text in the field + * will change to keep the same numeric value, but in the new input format. + * @param format the format for entering an integer into the field. + */ + public void setFormat(IntegerFormat format) { + if (!allFormats.contains(format)) { + throw new IllegalArgumentException(format.getName() + "is not valid for this field"); + } + BigInteger currentValue = getValue(); + currentFormat = format; + textField.setFormat(format); + setValue(currentValue); + } + + /** + * {@return the current format for entering numbers into this field} + */ + public IntegerFormat getFormat() { + return currentFormat; + } + + /** + * Returns the current text displayed in the field. + * + * @return the current text displayed in the field. + */ + public String getText() { + return textField.getText(); + } + + /** + * Returns the current minimum allowed value. Null indicates that there is no minimum value. + * + * @return the current maximum value allowed. + */ + public BigInteger getMinValue() { + return minValue; + } + + /** + * Returns the current maximum allowed value. Null indicates that there is no maximum value. + * + * @return the current maximum value allowed. + */ + public BigInteger getMaxValue() { + return maxValue; + } + + /** + * Returns the JTextField component that this class manages. + * + * @return the JTextField component that this class manages. + */ + public JComponent getComponent() { + return textField; + } + + /** + * Adds an ActionListener to the TextField. + * + * @param listener the ActionListener to add. + */ + public void addActionListener(ActionListener listener) { + textField.addActionListener(listener); + } + + /** + * Removes an ActionListener from the TextField. + * + * @param listener the ActionListener to remove. + */ + public void removeActionListener(ActionListener listener) { + textField.removeActionListener(listener); + } + + /** + * Sets the enablement on the JTextField component; + * + * @param enabled true for enabled, false for disabled. + */ + public void setEnabled(boolean enabled) { + textField.setEnabled(enabled); + } + + /** + * Sets the editable mode for the JTextField component + * + * @param editable boolean flag, if true component is editable + */ + public void setEditable(boolean editable) { + textField.setEditable(editable); + } + + /** + * Requests focus to the JTextField + */ + public void requestFocus() { + textField.requestFocus(); + } + + /** + * Selects the text in the JTextField + */ + public void selectAll() { + textField.selectAll(); + } + + /** + * Sets the horizontal alignment of the JTextField + * + * @param alignment the alignment as in {@link JTextField#setHorizontalAlignment(int)} + */ + public void setHorizontalAlignment(int alignment) { + textField.setHorizontalAlignment(alignment); + } + + /** + * 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 + */ + public void setUseNumberPrefix(boolean usePrefix) { + BigInteger value = getValue(); + this.usePrefix = usePrefix; + setValue(value); + } + + /** + * Returns a list of all support {@link IntegerFormat}s supported by this field. + * @return a list of all support number formats for this field. + */ + public List
getAllFormats() { + return new ArrayList<>(allFormats); + } + + protected void setMinValue(BigInteger minValue) { + BigInteger value = getValue(); + this.minValue = minValue; + setValue(value); + } + + protected void setMaxValue(BigInteger maxValue) { + BigInteger value = getValue(); + this.maxValue = maxValue; + setValue(value); + } + + private void valueChanged() { + for (ChangeListener listener : listeners) { + listener.stateChanged(new ChangeEvent(this)); + } + } + + private boolean allowsNegative() { + return minValue == null || minValue.compareTo(BigInteger.ZERO) < 0; + } + + protected boolean isInBounds(BigInteger value) { + if (minValue != null && minValue.compareTo(value) > 0) { + return false; + } + return maxValue == null || maxValue.compareTo(value) >= 0; + } + + private BigInteger parse(String text, IntegerFormat format) { + 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; + } + } + return format.parse(text); + } + + private boolean isValidPrefix(String text, IntegerFormat format) { + if (text.startsWith("-")) { + if (!allowsNegative()) { + return false; + } + if (text.length() == 1) { + return true; + } + text = text.substring(1); + } + if (!usePrefix) { + return false; + } + return usePrefix && format.getPrefix().startsWith(text); + } + + /** + * DocumentFilter that prevents users from entering invalid data into the field. + */ + private class HexDecimalDocumentFilter extends DocumentFilter { + @Override + public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr) + throws BadLocationException { + + // form a string that is the current document text with the inserted new text + text = text.replace('X', 'x'); + StringBuilder builder = getText(fb); + builder.insert(offset, text); + + // if the newly formed text is valid, allow the operation + if (isValid(builder.toString())) { + super.insertString(fb, offset, text, attr); + } + } + + @Override + public void replace(FilterBypass fb, int offset, int length, String text, + AttributeSet attrs) throws BadLocationException { + + // form a string that is the current document text with the replaced text + text = text.replace('X', 'x'); + StringBuilder builder = getText(fb); + builder.replace(offset, offset + length, text); + + // if the newly formed text is valid, allow the operation + if (isValid(builder.toString())) { + super.replace(fb, offset, length, text, attrs); + } + } + + @Override + public void remove(FilterBypass fb, int offset, int length) throws BadLocationException { + + // form a string that is the current document text with the indicated part deleted + StringBuilder builder = getText(fb); + builder.delete(offset, offset + length); + + // if the new formed text is valid, allow the operation. + if (isValid(builder.toString())) { + super.remove(fb, offset, length); + } + } + + private boolean isValid(String text) { + if (text.isEmpty()) { + return true; + } + if (isValidPrefix(text, currentFormat)) { + return true; + } + + BigInteger value = parse(text, currentFormat); + if (value != null) { + return isInBounds(value); + } + + if (usePrefix) { + // only allow auto switching if using number prefix + return autoSwitchFormat(text); + } + return false; + } + + private boolean autoSwitchFormat(String text) { + for (IntegerFormat format : allFormats) { + if (isValidPrefix(text, format)) { + currentFormat = format; + textField.setFormat(format); + return true; + } + BigInteger value = parse(text, format); + if (value != null && isInBounds(value)) { + currentFormat = format; + textField.setFormat(format); + return true; + } + } + return false; + } + + // Retrieves the current document text from inside the document filter. + private StringBuilder getText(FilterBypass fb) throws BadLocationException { + StringBuilder builder = new StringBuilder(); + Document document = fb.getDocument(); + builder.append(document.getText(0, document.getLength())); + return builder; + } + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/IntegerFormat.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/IntegerFormat.java new file mode 100644 index 0000000000..3409a1750a --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/IntegerFormat.java @@ -0,0 +1,107 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.widgets.textfield.integer; + +import java.math.BigInteger; + +import docking.widgets.textfield.FixedSizeIntegerTextField; +import docking.widgets.textfield.IntegerTextField; + +/** + * Input formats for entering integers into a text field such as the {@link IntegerTextField} or + * {@link FixedSizeIntegerTextField} + */ +public enum IntegerFormat { + DEC("Dec", "decimal", "", 10, false), + HEX("Hex", "hexadecimal", "0x", 16, false), + OCT("Oct", "octal", "0O", 8, false), + BIN("Bin", "binary", "0b", 2, false), + + U_DEC("uDec", "unsigned decimal", "", 10, true), + U_HEX("uHex", "unsigned hexadecimal", "0x", 16, true), + U_OCT("uOct", "unsigned octal", "0O", 8, true), + U_BIN("uBin", "unsigned binary", "0b", 2, true); + + private String name; + private String longName; + private String prefix; + private int radix; + private boolean isUnsigned; + + private IntegerFormat(String name, String description, String prefix, int radix, + boolean isUnsigned) { + this.name = name; + this.longName = description; + this.prefix = prefix; + this.radix = radix; + this.isUnsigned = isUnsigned; + } + + /** + * {@return the short name of this number format} + */ + public String getName() { + return name; + } + + /** + * {@return a descriptive name of this number format} + */ + public String getDescription() { + return longName; + } + + /** + * Converts the given value into a string representation corresponding to this number format. + * @param value the value to format into a string + * @return A string representation of the given value. + */ + public String format(BigInteger value) { + return value.toString(radix); + } + + /** + * Parses the given string into a BigInteger or null if the string is not properly structured + * for this number format. + * @param text the text to parse into a BigInteger + * @return the BigInteger interpretation of the given string for this number format. + */ + public BigInteger parse(String text) { + try { + return new BigInteger(text, radix); + } + catch (NumberFormatException e) { + // failed to parse, return null + } + return null; + } + + /** + * {@return the prefix associated with this format} + */ + public String getPrefix() { + return prefix; + } + + /** + * Return true if this format is intended only for non-negative values. This is more of a hint + * to the client text field to determine if "-" characters are allowed to be entered. + * @return true if this format is for unsigned numbers + */ + public boolean isUnsigned() { + return isUnsigned; + } +} 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 new file mode 100644 index 0000000000..1dd7a03560 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/integer/MultiFormatTextField.java @@ -0,0 +1,172 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.widgets.textfield.integer; + +import java.awt.*; +import java.awt.event.*; +import java.util.List; +import java.util.function.Consumer; + +import javax.swing.JTextField; +import javax.swing.ToolTipManager; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; + +import docking.DockingUtils; +import docking.util.GraphicsUtils; +import generic.theme.GThemeDefaults.Colors.Messages; +import generic.theme.Gui; +import ghidra.util.Swing; +import utility.function.Callback; + +/** + * Overrides the JTextField mainly to allow hint painting for the current input format. It also + * handles processing control-M to switch modes. + */ +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; + + public MultiFormatTextField(int columns, List formats, + Consumer formatChangeConsumer) { + super(columns); + + this.formats = formats; + updateFormatNameWidth(); + + addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_M && DockingUtils.isControlModifier(e)) { + if (++currentFormatIndex >= formats.size()) { + currentFormatIndex = 0; + } + updateFormatNameWidth(); + formatChangeConsumer.accept(formats.get(currentFormatIndex)); + repaint(); + } + } + }); + + // make sure tooltips will be activated + ToolTipManager.sharedInstance().registerComponent(this); + + } + + /** + * Uses the given callback to notify the client when the text has changed in this text field. + * @param c the callback to be notified when the text changes in this field + */ + public void addTextChangedCallback(Callback c) { + Document document = getDocument(); + + document.addDocumentListener(new DocumentListener() { + + @Override + public void removeUpdate(DocumentEvent e) { + Swing.runLater(() -> c.call()); + } + + @Override + public void insertUpdate(DocumentEvent e) { + Swing.runLater(() -> c.call()); + } + + @Override + public void changedUpdate(DocumentEvent e) { + Swing.runLater(() -> c.call()); + } + }); + } + + private void updateFormatNameWidth() { + FontMetrics fontMetrics = getFontMetrics(Gui.getFont(FONT_ID)); + hintWidth = fontMetrics.stringWidth(formats.get(currentFormatIndex).getName()); + + } + + @Override + public String getToolTipText(MouseEvent event) { + + 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 super.getToolTipText(event); + } + + /** + * 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) { + int indexOf = formats.indexOf(format); + if (indexOf >= 0) { + currentFormatIndex = indexOf; + } + updateFormatNameWidth(); + repaint(); + } + + /** + * Turns on or off the faded hint text that displays the field's current format (i.e., hex, + * decimal). + * + * @param show true to show the input format. + */ + public void setShowInputFormatHint(boolean show) { + this.showFormatHint = show; + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + if (!showFormatHint) { + return; + } + + Font savedFont = g.getFont(); + g.setFont(Gui.getFont(FONT_ID)); + g.setColor(Messages.HINT); + + Dimension size = getSize(); + Insets insets = getInsets(); + int x; + if (getHorizontalAlignment() == RIGHT) { + x = insets.left; + } + else { + 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); + 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 57d89707cf..039b208a14 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 @@ -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. @@ -18,6 +18,7 @@ package docking.widgets.values; import javax.swing.JComponent; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; /** * Value class for {@link Integer} Value with an option for display the value as decimal or hex. The @@ -69,10 +70,10 @@ public class IntValue extends AbstractValue { public JComponent getComponent() { if (field == null) { field = new IntegerTextField(20); - field.setAllowsHexPrefix(false); + field.setUseNumberPrefix(false); field.setShowNumberMode(false); if (displayAsHex) { - field.setHexMode(); + field.setFormat(IntegerFormat.HEX); field.setShowNumberMode(true); } } 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 be6680bfe1..57a27007c3 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 @@ -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. @@ -18,6 +18,7 @@ package docking.widgets.values; import javax.swing.JComponent; import docking.widgets.textfield.IntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; /** * Value class for Long Values with an option for display the value as decimal or hex. The @@ -57,10 +58,10 @@ public class LongValue extends AbstractValue { public JComponent getComponent() { if (field == null) { field = new IntegerTextField(20); - field.setAllowsHexPrefix(false); + field.setUseNumberPrefix(false); field.setShowNumberMode(false); if (displayAsHex) { - field.setHexMode(); + field.setFormat(IntegerFormat.HEX); field.setShowNumberMode(true); } } 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 new file mode 100644 index 0000000000..b35557ff07 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/AbstractIntegerTextFieldTest.java @@ -0,0 +1,115 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.widgets.textfield; + +import java.math.BigInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import org.junit.After; +import org.junit.Before; + +import docking.test.AbstractDockingTest; +import docking.widgets.textfield.integer.AbstractIntegerTextField; +import docking.widgets.textfield.integer.IntegerFormat; + +public abstract class AbstractIntegerTextFieldTest + extends AbstractDockingTest { + private JFrame frame; + protected T field; + protected JTextField textField; + + @Before + public void setUp() throws Exception { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + field = createField(); + field.setShowNumberMode(true); + textField = (JTextField) field.getComponent(); + frame = new JFrame("Test"); + frame.getContentPane().add(field.getComponent()); + frame.pack(); + frame.setVisible(true); + } + + abstract protected T createField(); + + @After + public void tearDown() throws Exception { + frame.setVisible(false); + } + + protected void setFormat(IntegerFormat format) { + runSwing(() -> field.setFormat(format)); + } + + protected IntegerFormat getFormat() { + return runSwing(() -> field.getFormat()); + } + + protected void setValue(long value) { + runSwing(() -> field.setValue(value)); + } + + protected long getValue() { + return runSwing(() -> field.getLongValue()); + } + + protected int getIntValue() { + return runSwing(() -> field.getIntValue()); + } + + protected BigInteger getBigIntegerValue() { + return runSwing(() -> field.getValue()); + } + + protected String getText() { + return runSwing(() -> field.getText()); + } + + protected BigInteger getMinValue() { + return runSwing(() -> field.getMinValue()); + } + + protected BigInteger getMaxValue() { + return runSwing(() -> field.getMaxValue()); + } + + protected void setText(String text) { + runSwing(() -> field.setText(text)); + } + + protected void typeText(String text) { + triggerText(textField, text); + } + + protected void setUsePrefix(boolean b) { + runSwing(() -> field.setUseNumberPrefix(b)); + } + + class TestChangeListener implements ChangeListener { + volatile int count; + protected AtomicIntegerArray values = new AtomicIntegerArray(10); + + @Override + public void stateChanged(ChangeEvent e) { + values.set(count++, field.getIntValue()); + } + + } +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/FixedSizeIntegerTextFieldTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/FixedSizeIntegerTextFieldTest.java new file mode 100644 index 0000000000..3bb72bba3a --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/FixedSizeIntegerTextFieldTest.java @@ -0,0 +1,433 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.widgets.textfield; + +import static docking.widgets.textfield.integer.IntegerFormat.*; +import static org.junit.Assert.*; + +import java.math.BigInteger; + +import org.junit.Test; + +public class FixedSizeIntegerTextFieldTest + extends AbstractIntegerTextFieldTest { + @Override + protected FixedSizeIntegerTextField createField() { + return new FixedSizeIntegerTextField(10, 8); + } + + @Test + public void testSignedHexSetValue() { + setFormat(HEX); + setValue(25); + assertEquals("0x19", getText()); + setValue(-1); + assertEquals("-0x1", getText()); + } + + @Test + public void testSignedHexSetText() { + setFormat(HEX); + setText("0x12"); + assertEquals(18, getValue()); + setText(""); + assertEquals(0, getValue()); + } + + @Test + public void testSignedHexTypeText() { + setFormat(HEX); + typeText("mnp0x12"); + assertEquals(18, getValue()); + setText(""); + typeText("-0x5"); + assertEquals(-5, getValue()); + } + + @Test + public void testSignedHexMin() { + setFormat(HEX); + setBitSize(8); + + typeText("-0x8"); + assertEquals("-0x8", getText()); + typeText("1"); + assertEquals("-0x8", getText()); // -0x80 (-128) is min allowed for 8 bits + typeText("0"); + assertEquals("-0x80", getText()); + assertEquals(-128, getValue()); + } + + @Test + public void testSignedHexMax() { + setFormat(HEX); + setBitSize(8); + + typeText("0x8"); + assertEquals("0x8", getText()); + typeText("0"); + assertEquals("0x8", getText()); // 7f (127) is the max allowed for 8 bits + setText(""); + typeText("0x7f"); + assertEquals("0x7f", getText()); + assertEquals(127, getValue()); + typeText("1"); + assertEquals("0x7f", getText()); + } + + @Test + public void testSignedHexConvertsUnsignedValueToNegative() { + setFormat(HEX); + setBitSize(8); + + setValue(0xff); + assertEquals(-1, getValue()); + } + + @Test + public void testUnsignedHexSetValue() { + setFormat(U_HEX); + setValue(25); + assertEquals("0x19", getText()); + setValue(-130); + assertEquals("", getText()); + setValue(0xff); + assertEquals("0xff", getText()); + } + + @Test + public void testUnsignedHexSetText() { + setFormat(U_HEX); + + setText("0x12"); + assertEquals(18, getValue()); + setText("-0x1"); + assertEquals(18, getValue()); // didn't change the setText was ignored + setText("0xff"); + assertEquals(255, getValue()); + } + + @Test + public void testUnsignedHexTypeText() { + setFormat(U_HEX); + typeText("mnp0x12"); + setText("0x13"); + assertEquals(19, getValue()); + } + + @Test + public void testunSignedHexMin() { + setFormat(U_HEX); + setBitSize(8); + + typeText("-0x8"); + assertEquals("0x8", getText()); + setText(""); + typeText("0x0"); + assertEquals("0x0", getText()); + assertEquals(0, getValue()); + } + + @Test + public void testunSignedHexMax() { + setFormat(U_HEX); + setBitSize(8); + + typeText("0xff"); + assertEquals("0xff", getText()); + assertEquals(255, getValue()); + setText(""); + typeText("0x10"); + assertEquals("0x10", getText()); + setText("0x10"); + typeText("0"); + assertEquals("0x10", getText()); + } + + @Test + public void testUnsignedHexConvertsUnsignedValueToNegative() { + setFormat(U_HEX); + setBitSize(8); + setValue(-1); + assertEquals("0xff", getText()); + assertEquals(255, getValue()); + } + + @Test + public void testSignedDecimalSetText() { + setFormat(DEC); + setText("12"); + assertEquals(12, getValue()); + setText("-4"); + assertEquals(-4, getValue()); + } + + @Test + public void testSignedDecimalTypeText() { + setFormat(DEC); + typeText("mnp12"); + setText("12"); + assertEquals(12, getValue()); + setText(""); + typeText("-5"); + assertEquals(-5, getValue()); + } + + @Test + public void testSignedDecimalMin() { + setFormat(DEC); + setBitSize(8); + + typeText("-128"); + assertEquals("-128", getText()); + setText(""); + typeText("-129"); + assertEquals("-12", getText()); + } + + @Test + public void testSignedDecimalMax() { + setFormat(DEC); + setBitSize(8); + + typeText("127"); + assertEquals("127", getText()); + assertEquals(127, getValue()); + typeText("3"); + assertEquals("127", getText()); // 7f (127) is the max allowed for 8 bits + setText(""); + typeText("128"); + assertEquals("12", getText()); + assertEquals(12, getValue()); + } + + @Test + public void testSignedDecimalConvertsUnsignedValueToNegative() { + setFormat(DEC); + setBitSize(8); + + setValue(255); + assertEquals(-1, getValue()); + } + + @Test + public void testUnsignedDecimalSetValue() { + setFormat(U_DEC); + setValue(25); + assertEquals("25", getText()); + setValue(-130); + assertEquals("", getText()); + setValue(255); + assertEquals("255", getText()); + } + + @Test + public void testUnsignedDecimalSetText() { + setFormat(U_DEC); + + setText("12"); + assertEquals(12, getValue()); + setText("-0x1"); + assertEquals(12, getValue()); // didn't change the setText was ignored + setText("255"); + assertEquals(255, getValue()); + } + + @Test + public void testUnsignedDecimalTypeText() { + setFormat(U_DEC); + typeText("mnp12"); + setText("12"); + assertEquals(12, getValue()); + } + + @Test + public void testUnSignedDecimalMin() { + setFormat(U_DEC); + setBitSize(8); + + typeText("-8"); + assertEquals("8", getText()); + setText(""); + typeText("0"); + assertEquals("0", getText()); + assertEquals(0, getValue()); + } + + @Test + public void testUnsignedDecimalMax() { + setFormat(U_DEC); + setBitSize(8); + + typeText("255"); + assertEquals("255", getText()); + assertEquals(255, getValue()); + setText(""); + typeText("256"); + assertEquals("25", getText()); + } + + @Test + public void testUnsignedDecimalConvertsUnsignedValueToNegative() { + setFormat(U_DEC); + setBitSize(8); + setValue(-1); + assertEquals("255", getText()); + assertEquals(255, getValue()); + } + + @Test + public void testUnsignedOctSetValue() { + setFormat(U_OCT); + setValue(25); + assertEquals("0O31", getText()); + setValue(-1); + assertEquals("0O377", getText()); + } + + @Test + public void testUnsignedOctSetText() { + setFormat(U_OCT); + setText("0O12"); + assertEquals(10, getValue()); + setText(""); + assertEquals(0, getValue()); + } + + @Test + public void testSignedOctTypeText() { + setFormat(U_OCT); + typeText("mnp0O12"); + assertEquals(10, getValue()); + setText(""); + typeText("-0O5"); // "-" will be ignored + assertEquals(5, getValue()); + } + + @Test + public void testUnsignedBinarySetValue() { + setFormat(U_BIN); + setValue(0x25); + assertEquals("0b100101", getText()); + setValue(-1); + assertEquals("0b11111111", getText()); + } + + @Test + public void testUnsignedBinarySetText() { + setFormat(U_BIN); + setText("0b101"); + assertEquals(5, getValue()); + setText(""); + assertEquals(0, getValue()); + } + + @Test + public void testSignedBinaryTypeText() { + setFormat(U_BIN); + typeText("mnp0b1210"); // the mnp and 2 are ignored + assertEquals("0b110", getText()); + assertEquals(6, getValue()); + setText(""); + typeText("-0b101"); // "-" will be ignored + assertEquals(5, getValue()); + } + + @Test + public void testChangingBitSizeSigned() { + setFormat(DEC); + setBitSize(8); + setValue(25); + assertEquals("25", getText()); + assertEquals(BigInteger.valueOf(-128), getMinValue()); + assertEquals(BigInteger.valueOf(127), getMaxValue()); + + setBitSize(16); + assertEquals("25", getText()); + assertEquals(BigInteger.valueOf(-32768), getMinValue()); + assertEquals(BigInteger.valueOf(32767), getMaxValue()); + + setValue(32000); + assertEquals("32000", getText()); + setBitSize(8); + assertEquals(BigInteger.valueOf(-128), getMinValue()); + assertEquals(BigInteger.valueOf(127), getMaxValue()); + assertEquals("", getText()); + } + + @Test + public void testChangingBitSizeUnsigned() { + setFormat(U_DEC); + setBitSize(8); + setValue(25); + assertEquals("25", getText()); + assertEquals(BigInteger.valueOf(0), getMinValue()); + assertEquals(BigInteger.valueOf(255), getMaxValue()); + + setBitSize(16); + assertEquals("25", getText()); + assertEquals(BigInteger.valueOf(0), getMinValue()); + assertEquals(BigInteger.valueOf(65535), getMaxValue()); + + setValue(65000); + assertEquals("65000", getText()); + setBitSize(8); + assertEquals(BigInteger.valueOf(0), getMinValue()); + assertEquals(BigInteger.valueOf(255), getMaxValue()); + assertEquals("", getText()); + } + + @Test + public void testChangingFromSignedToUnsignedUpperBitNotSet() { + setFormat(DEC); + setBitSize(8); + setValue(8); + setFormat(U_DEC); + assertEquals(8, getValue()); + } + + @Test + public void testChangingFromSignedToUnsingedUpperBitSet() { + setFormat(DEC); + setValue(-1); + assertEquals(-1, getValue()); + setFormat(U_DEC); + assertEquals(255, getValue()); + + } + + @Test + public void testChangingFromUnsignedToSignedUpperBitNotSet() { + setFormat(U_DEC); + setBitSize(8); + setValue(8); + setFormat(DEC); + assertEquals(8, getValue()); + } + + @Test + public void testChangingFromUnsignedToSingedUpperBitSet() { + setFormat(U_DEC); + setValue(255); + assertEquals(255, getValue()); + setFormat(DEC); + assertEquals(-1, getValue()); + } + + protected void setBitSize(int value) { + runSwing(() -> field.setBitSize(value)); + } +} 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 512c32f27c..4b3e9f4d61 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 @@ -15,177 +15,156 @@ */ package docking.widgets.textfield; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import static docking.widgets.textfield.integer.IntegerFormat.*; +import static org.junit.Assert.*; import java.math.BigInteger; -import java.util.concurrent.atomic.AtomicIntegerArray; -import javax.swing.JFrame; -import javax.swing.JTextField; -import javax.swing.UIManager; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import org.junit.After; -import org.junit.Before; import org.junit.Test; -import docking.test.AbstractDockingTest; +public class IntegerTextFieldTest extends AbstractIntegerTextFieldTest { -public class IntegerTextFieldTest extends AbstractDockingTest { - - private JFrame frame; - private IntegerTextField field; - private JTextField textField; - - @Before - public void setUp() throws Exception { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - field = new IntegerTextField(10); - field.setShowNumberMode(true); - textField = (JTextField) field.getComponent(); - frame = new JFrame("Test"); - frame.getContentPane().add(field.getComponent()); - frame.pack(); - frame.setVisible(true); - } - - @After - public void tearDown() throws Exception { - frame.setVisible(false); + @Override + protected IntegerTextField createField() { + return new IntegerTextField(10); } @Test public void testDefaultState() { - assertNull(field.getValue());// no value - assertEquals(0, field.getIntValue());// the "int value" return for null is 0 - assertEquals(0, field.getLongValue()); - assertTrue(!field.isHexMode()); - assertNull(field.getMaxValue()); + assertNull(getBigIntegerValue());// no value + assertEquals(0, getIntValue()); // the "int value" return for null is 0 + assertEquals(0, getValue()); + assertEquals(DEC, getFormat()); + assertNull(getMaxValue()); } @Test public void testTypeValidDecimalNumber() { - triggerText(textField, "123"); - assertEquals(123, field.getIntValue()); + typeText("123"); + assertEquals(123, getIntValue()); } @Test public void testTypeValidHexNumber() { - triggerText(textField, "0x2abcdef"); - assertEquals(0x2abcdef, field.getIntValue()); + typeText("0x2abcdef"); + assertEquals(0x2abcdef, getValue()); } @Test public void testInvalidCharsIgnored() { - triggerText(textField, "123ghijklmnopqrstuvwxyz4"); - assertEquals(1234, field.getIntValue()); + typeText("123ghijklmnopqrstuvwxyz4"); + assertEquals(1234, getValue()); } @Test public void testHexCharsIgnoredInDecimalMode() { - assertTrue(!field.isHexMode()); - triggerText(textField, "123ghijklmnopqrstuvwxyz4"); - assertEquals(1234, field.getIntValue()); + setFormat(HEX); + typeText("123ghijklmnopqrstuvwxyz4"); + assertEquals(1234, getValue()); } @Test public void testXchangesHexMode() { - assertTrue(!field.isHexMode()); - triggerText(textField, "0"); - assertTrue(!field.isHexMode()); - triggerText(textField, "x"); - assertTrue(field.isHexMode()); + assertEquals(DEC, getFormat()); + typeText("0"); + assertEquals(DEC, getFormat()); + typeText("x"); + assertEquals(HEX, getFormat()); triggerBackspace(textField); - assertTrue(!field.isHexMode()); + assertEquals(HEX, getFormat()); } @Test public void testHexModeWithoutPrefix() { - triggerText(textField, "abc");// not allowed when using hex prefix, so expect empty - assertEquals(null, field.getValue()); + setFormat(HEX); + typeText("a"); + assertEquals(null, getBigIntegerValue()); - field.setAllowsHexPrefix(false); - field.setHexMode(); - triggerText(textField, "abc"); - assertEquals(0xabc, field.getIntValue()); + setUsePrefix(false); + typeText("abc"); + assertEquals(0xabc, getValue()); } @Test public void testNegative() { - triggerText(textField, "-123"); - assertEquals(-123, field.getIntValue()); + typeText("-123"); + assertEquals(-123, getValue()); } @Test public void testNegativeHex() { - triggerText(textField, "-0xa"); - assertEquals(-10, field.getIntValue()); + typeText("-0xa"); + assertEquals(-10, getValue()); } @Test public void testNegativeNotAllowed() { - field.setAllowNegativeValues(false); - triggerText(textField, "-123"); - assertEquals(123, field.getIntValue()); + setMinValue(BigInteger.ZERO); + typeText("-123"); + assertEquals(123, getValue()); } @Test public void testSetNegativeWithCurrentNegativeValue() { - field.setValue(-123); - field.setAllowNegativeValues(false); + setValue(-123); + setMinValue(BigInteger.ZERO); assertEquals(null, field.getValue()); } @Test public void testMax() { field.setMaxValue(BigInteger.valueOf(13l)); - triggerText(textField, "12"); - assertEquals(12, field.getIntValue()); + typeText("12"); + assertEquals(12, getValue()); - field.setValue(null); - triggerText(textField, "13"); - assertEquals(13, field.getIntValue()); + setText(""); + typeText("13"); + assertEquals(13, getValue()); - field.setValue(null); - triggerText(textField, "14");// four should be ignored - assertEquals(1, field.getIntValue()); + setText(""); + typeText("14");// four should be ignored + assertEquals(1, getValue()); } @Test public void testSetMaxToValueSmallerThanCurrent() { - field.setValue(500); + setValue(500); field.setMaxValue(BigInteger.valueOf(400)); - assertEquals(400, field.getIntValue()); + assertNull(field.getValue()); + } + + @Test + public void testMinSetTo1() { + field.setMinValue(BigInteger.ONE); + setValue(0); + assertEquals(1, getValue()); + } @Test public void testMaxInHex() { field.setMaxValue(BigInteger.valueOf(0xd)); - triggerText(textField, "0xc"); - assertEquals(12, field.getIntValue()); + typeText("0xc"); + assertEquals(12, getValue()); - field.setValue(null); - triggerText(textField, "0xd"); - assertEquals(13, field.getIntValue()); + setText(""); + typeText("0xd"); + assertEquals(13, getValue()); - field.setValue(null); - triggerText(textField, "0xe");// e should be ignored - assertEquals(0, field.getIntValue()); + setText(""); + typeText("0xe");// e should be ignored + assertEquals(0, getValue()); } @Test public void testSwitchingHexMode() { - field.setValue(255); + setValue(255); assertEquals("255", field.getText()); - field.setHexMode(); + setFormat(HEX); assertEquals("0xff", field.getText()); - field.setDecimalMode(); + setFormat(DEC); assertEquals("255", field.getText()); } @@ -194,7 +173,7 @@ public class IntegerTextFieldTest extends AbstractDockingTest { TestChangeListener listener = new TestChangeListener(); field.addChangeListener(listener); - triggerText(textField, "123"); + typeText("123"); assertEquals(3, listener.count); assertEquals(1, listener.values.get(0)); assertEquals(12, listener.values.get(1)); @@ -207,129 +186,149 @@ public class IntegerTextFieldTest extends AbstractDockingTest { @Test public void testChangeListenerAfterSwitchingModes() { - triggerText(textField, "123"); + setFormat(DEC); + typeText("12"); TestChangeListener listener = new TestChangeListener(); field.addChangeListener(listener); - setHexMode(); + setFormat(HEX); + assertEquals("0xc", getText()); assertEquals(2, listener.count); - assertEquals(123, listener.values.get(1)); + assertEquals(12, listener.values.get(1)); } @Test public void testNegativeHexFromValue() { - field.setValue(-255); - setHexMode(); + setValue(-255); + setFormat(HEX); assertEquals("-0xff", field.getText()); } @Test public void testNullValue() { - field.setValue(12); + setValue(12); assertEquals("12", field.getText()); - field.setValue(null); + setText(""); assertEquals("", field.getText()); - assertEquals(0, field.getIntValue()); - assertEquals(0l, field.getLongValue()); - assertEquals(null, field.getValue()); + assertEquals(0, getValue()); + assertEquals(null, getBigIntegerValue()); } @Test public void testHexValueInDontRequireHexPrefixMode() { - field.setAllowsHexPrefix(false); - field.setHexMode(); - field.setValue(255); + field.setUseNumberPrefix(false); + field.setFormat(HEX); + setValue(255); assertEquals("ff", field.getText()); } + @Test + public void testAutoModeSwitchingIsOffWhenPrefixNotUsed() { + field.setUseNumberPrefix(false); + field.setFormat(HEX); + typeText("15"); + assertEquals(HEX, getFormat()); + assertEquals(21, getValue()); + field.setFormat(DEC); + field.setText(""); + typeText("0x15"); + assertEquals(DEC, getFormat()); + assertEquals("015", getText()); + assertEquals(15, getValue()); // the 0x should have been ignored + } + @Test public void testSetNotAllowNegativeModeWhileCurrentValueIsNegative() { - field.setValue(-10); - field.setAllowNegativeValues(false); + setValue(-10); + setMinValue(BigInteger.ZERO); assertEquals("", field.getText()); - assertEquals(0, field.getIntValue()); + assertEquals(0, getValue()); } @Test public void testSetLongValue() { - field.setValue(100L); + setValue(100L); assertEquals(100L, field.getLongValue()); - assertEquals(100, field.getIntValue()); + assertEquals(100, getValue()); } @Test public void testSettingNegativeNumberWhenNegativesArentAllowed() { - field.setValue(10); - field.setAllowNegativeValues(false); - field.setValue(-10); + setValue(10); + setMinValue(BigInteger.ZERO); + setValue(-10); assertEquals("", field.getText()); } @Test public void testUseHexPrefixUpdatesTextField() { - field.setAllowsHexPrefix(false); - field.setHexMode(); - field.setValue(255); + field.setUseNumberPrefix(false); + setFormat(HEX); + setValue(255); assertEquals("ff", field.getText()); - field.setAllowsHexPrefix(true); + field.setUseNumberPrefix(true); assertEquals("0xff", field.getText()); } @Test public void testPastingBadText() { - field.setHexMode(); - field.setValue(0); + setFormat(HEX); + setValue(0); assertFalse(field.setText("asdf 0x azzz")); } @Test public void testSetText() { - field.setHexMode(); - field.setValue(0); + setFormat(HEX); + setValue(0); assertTrue(field.setText("0x15")); - assertEquals(0x15, field.getIntValue()); + assertEquals(0x15, getValue()); } @Test public void testSetTextWithInvalidValue() { - field.setHexMode(); - field.setValue(0); + setFormat(HEX); + setValue(0); assertFalse(field.setText("bad value")); - assertEquals(0, field.getIntValue()); - assertTrue(field.isHexMode()); + assertEquals(0, getValue()); + assertEquals(HEX, getFormat()); } @Test public void testSucessfulSetTextChangesHexMode() { - field.setHexMode(); - field.setValue(0); + setFormat(HEX); + setValue(0); assertTrue(field.setText("33")); - assertEquals(33, field.getIntValue()); - assertFalse(field.isHexMode()); + assertEquals(33, getValue()); + assertEquals(DEC, getFormat()); assertTrue(field.setText("0x33")); - assertEquals(0x33, field.getIntValue()); - assertTrue(field.isHexMode()); - + assertEquals(0x33, getValue()); + assertEquals(HEX, getFormat()); } - private void setHexMode() { - runSwing(() -> field.setHexMode()); - waitForSwing(); + @Test + public void testMinValueOfOneDecimalFormat() { + setFormat(DEC); + field.setMinValue(BigInteger.ONE); + typeText("0"); + assertEquals("", field.getText()); + typeText("1"); + assertEquals("1", field.getText()); } - class TestChangeListener implements ChangeListener { - volatile int count; - private AtomicIntegerArray values = new AtomicIntegerArray(10); - - @Override - public void stateChanged(ChangeEvent e) { - values.set(count++, field.getIntValue()); - } - + @Test + public void testMinValueOfOneHexFormat() { + setFormat(HEX); + field.setMinValue(BigInteger.ONE); + typeText("0x1"); + assertEquals("0x1", field.getText()); } + protected void setMinValue(BigInteger minValue) { + runSwing(() -> field.setMinValue(minValue)); + } }