diff --git a/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java b/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java new file mode 100644 index 0000000000..8e540e7e5b --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java @@ -0,0 +1,92 @@ +/* ### + * 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. + */ +// Example script for showing how to use the "AskValues" script method for inputing multiple values +// @category Examples +import ghidra.app.script.GhidraScript; +import ghidra.features.base.values.GhidraValuesMap; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.util.MessageType; + +public class AskValuesExampleScript extends GhidraScript { + @Override + public void run() throws Exception { + + GhidraValuesMap values = new GhidraValuesMap(); + + values.defineString("Name"); + values.defineAddress("Address", currentProgram); + values.defineInt("Count"); + values.defineInt("Max Results", 100); + values.defineChoice("Priority", "Low", "Low", "Medium", "High"); + + // When asking for a program, you must supply a consumer that you will use + // to release the program. Since programs share open instances, Ghidra uses + // consumers to keep track of these uses. Scripts can just add themselves + // as the consumer (The askProgram() method does this for you). It is + // important to release it when you are done with. Optionally, you can also + // provide a tool in which case the program will also be opened in the tool (and the + // tool would then also add itself as a consumer). Otherwise, the program will not + // show up in the tool and when you release the consumer, it will be closed. + values.defineProgram("Other Program", this, state.getTool()); + + // Optional validator that can be set to validate values before the dialog returns. It + // is called when the "Ok" button is pushed and must return true before the dialog exits. + // It also includes a statusListener where messages can be set on the dialog. In this + // example, we are requiring that the name and program fields be populated + + values.setValidator((valueMap, status) -> { + if (!valueMap.hasValue("Name")) { + status.setStatusText("Name must be filled in!", MessageType.ERROR); + return false; + } + if (!valueMap.hasValue("Other Program")) { + status.setStatusText("Other Program must be filled it!", MessageType.ERROR); + return false; + } + return true; + }); + + // asks the script to show a dialog where the user can give values for all the items + // in the ValuesMap. + + values = askValues("Enter Example Script Values", null, values); + + // if the user cancels the ask dialog, the script will exit as cancelled. Otherwise + // the returned ValuesMap will contain the results of the user filling in values from the + //dialog. The values map returned may or may not be the same instance as the one passed in. + + String name = values.getString("Name"); + Address address = values.getAddress("Address"); + int age = values.getInt("Count"); + int max = values.getInt("Max Results"); + String priority = values.getChoice("Priority"); + Program program = values.getProgram("Other Program"); + + println("Name = " + name); + println("Address = " + address); + println("Count = " + age); + println("Max Results = " + max); + println("Priority = " + priority); + println("Program = " + program); + + // VERY IMPORTANT!!! you must release any programs when you are done with them! + // If you also opened in the tool, you can immediately release it because the tool will + // then keep it open. + program.release(this); + + } +} diff --git a/Ghidra/Features/Base/ghidra_scripts/ReloadSleighLanguage.java b/Ghidra/Features/Base/ghidra_scripts/ReloadSleighLanguage.java index dfdb6c603e..b65be066ee 100644 --- a/Ghidra/Features/Base/ghidra_scripts/ReloadSleighLanguage.java +++ b/Ghidra/Features/Base/ghidra_scripts/ReloadSleighLanguage.java @@ -33,8 +33,7 @@ public class ReloadSleighLanguage extends GhidraScript { language.reloadLanguage(monitor); } catch (IOException e) { - Msg.showError(this, this.state.getParamPanel(), "Reload Sleigh Language Failed", - e.getMessage()); + Msg.showError(this, null, "Reload Sleigh Language Failed", e.getMessage()); return; } currentProgram.setLanguage(language, null, true, monitor); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GatherParamPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GatherParamPanel.java deleted file mode 100644 index 92482a4a29..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GatherParamPanel.java +++ /dev/null @@ -1,189 +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 ghidra.app.script; - -import java.awt.Component; -import java.awt.GridLayout; -import java.io.File; -import java.util.HashMap; - -import javax.swing.JPanel; -import javax.swing.JTextField; - -import docking.widgets.filechooser.GhidraFileChooserMode; -import docking.widgets.filechooser.GhidraFileChooserPanel; -import docking.widgets.label.GLabel; -import ghidra.app.util.AddressInput; - -public class GatherParamPanel extends JPanel { - private static final long serialVersionUID = 1L; - - public static final int STRING = 0; - public static final int FILE = 1; - public static final int DIRECTORY = 2; - public static final int ADDRESS = 3; - public static final int INTEGER = 4; - public static final int LANGUAGE = 5; - - private GhidraState state; - private HashMap parameters; - private boolean shown; - - public GatherParamPanel(GhidraState state) { - this.state = state; - setLayout(new GridLayout(0, 2)); - parameters = new HashMap<>(); - shown = false; - } - - public ParamComponent getParameter(String key) { - return parameters.get(key); - } - - public void clearParameters() { - parameters.clear(); - removeAll(); - } - - public void addParameterRegardless(String key, String label, int type, Object defaultValue) { - Component displayComponent = null; - if (type == FILE || type == DIRECTORY) { - String titleString = null; - if (type == DIRECTORY) { - titleString = "SELECT DIRECTORY"; - } - else { - titleString = "SELECT FILE"; - } - GhidraFileChooserPanel panel = new GhidraFileChooserPanel(titleString, - "Recipe.fileChooser", "", true, GhidraFileChooserPanel.INPUT_MODE); - if (type == DIRECTORY) { - panel.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); - } - panel.setFileName(defaultValue.toString()); - parameters.put(key, new ParamComponent(panel, type)); - displayComponent = panel; - } - else if (type == ADDRESS) { - AddressInput addressInput = new AddressInput(); - if (state.getCurrentProgram() != null) { - addressInput.setAddressFactory(state.getCurrentProgram().getAddressFactory()); - } - addressInput.selectDefaultAddressSpace(); - addressInput.select(); - if (defaultValue != null) { - addressInput.setValue(defaultValue.toString()); - } - displayComponent = addressInput; - parameters.put(key, new ParamComponent(displayComponent, type)); - } - else { - JTextField textField = new JTextField(); - if (defaultValue != null) { - textField.setText(defaultValue.toString()); - } - displayComponent = textField; - parameters.put(key, new ParamComponent(displayComponent, type)); - } - add(new GLabel(label)); - add(displayComponent); - shown = false; - } - - public void addParameter(String key, String label, int type, Object defaultValue) { - if (parameters.containsKey(key) || state.getEnvironmentVar(key) != null) { - return; - } - addParameterRegardless(key, label, type, defaultValue); - } - - public void setParamsInState() { - for (String string2 : parameters.keySet()) { - String key = string2.toString(); - ParamComponent pc = parameters.get(key); - switch (pc.getType()) { - case ADDRESS: - if (state.getCurrentProgram() != null) { - AddressInput addressInput = (AddressInput) pc.getDisplayComponent(); - state.addEnvironmentVar(key, addressInput.getAddress()); - } - else { - AddressInput addressInput = (AddressInput) pc.getDisplayComponent(); - state.addEnvironmentVar(key, addressInput.getValue().toString()); - } - break; - case FILE: - case DIRECTORY: - GhidraFileChooserPanel gfcp = (GhidraFileChooserPanel) pc.getDisplayComponent(); - state.addEnvironmentVar(key, new File(gfcp.getFileName())); - break; - case INTEGER: - JTextField iTextField = (JTextField) pc.getDisplayComponent(); - int val = Integer.parseInt(iTextField.getText()); - state.addEnvironmentVar(key, val); - break; - default: - JTextField textField = (JTextField) pc.getDisplayComponent(); - state.addEnvironmentVar(key, textField.getText()); - break; - } - } - } - - public void currentProgramChanged() { - for (String string2 : parameters.keySet()) {//OMG!! - String key = string2.toString(); - ParamComponent pc = parameters.get(key); - switch (pc.getType()) { - case ADDRESS: - AddressInput addressInput = (AddressInput) pc.getDisplayComponent(); - addressInput.setAddressFactory(state.getCurrentProgram().getAddressFactory()); - addressInput.selectDefaultAddressSpace(); - addressInput.select(); - if (panelShown()) { - state.addEnvironmentVar(key, addressInput.getAddress()); - } - break; - } - } - } - - public boolean panelShown() { - return shown; - } - - public void setShown(boolean shown) { - this.shown = shown; - } - - public class ParamComponent { - private int type; - private Component displayComponent; - - public ParamComponent(Component displayComponent, int type) { - this.displayComponent = displayComponent; - this.type = type; - } - - public Component getDisplayComponent() { - return displayComponent; - } - - public int getType() { - return type; - } - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java index eae0a4e6ff..45ade2409d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java @@ -20,12 +20,15 @@ import java.io.*; import java.rmi.ConnectException; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import docking.DockingWindowManager; import docking.widgets.OptionDialog; import docking.widgets.PasswordDialog; import docking.widgets.dialogs.MultiLineMessageDialog; import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooserMode; +import docking.widgets.values.*; import generic.jar.ResourceFile; import generic.theme.GThemeDefaults.Colors.Palette; import ghidra.app.plugin.core.analysis.AnalysisWorker; @@ -44,6 +47,7 @@ import ghidra.app.util.opinion.*; import ghidra.app.util.query.TableService; import ghidra.app.util.viewer.field.BrowserCodeUnitFormat; import ghidra.app.util.viewer.field.CommentUtils; +import ghidra.features.base.values.GhidraValuesMap; import ghidra.framework.Application; import ghidra.framework.client.*; import ghidra.framework.cmd.BackgroundCommand; @@ -2327,6 +2331,66 @@ public abstract class GhidraScript extends FlatProgramAPI { } } + /** + * Prompts for multiple values at the same time. To use this method, you must first + * create a {@link GhidraValuesMap} and define the values that will be supplied by this method. + * In the GUI environment, this will result in a single dialog with an entry for each value + * defined in the values map. This method returns a GhidraValuesMap with the values supplied by + * the user in GUI mode or command line arguments in headless mode. If the user cancels the + * dialog, a cancelled exception will be thrown, and unless it is explicity caught by the + * script, will terminate the script. Also, if the values map has a {@link ValuesMapValidator}, + * the values will be validated when the user presses the "OK" button and will only exit the + * dialog if the validate check passes. Otherwise, the validator should have reported an error + * message in the dialog and the dialog will remain visible. + * + *

+ * Regardless of environment -- if script arguments have been set, this method will use the + * next arguments in the array and advance the array index until all values in the values map + * have been satisfied and so the next call to an ask method will get the next argument after + * those consumed by this call. + * + * @param title the title of the dialog if in GUI mode + * @param optionalMessage an optional message that is displayed in the dialog, just above the + * list of name/value pairs + * @param values the GhidraValuesMap containing the values to include in the dialog. + * @return the GhidraValuesMap with values set from user input in the dialog (This is the same + * instance that was passed in, so you don't need to use this) + * @throws CancelledException if the user hit the 'cancel' button in GUI mode + */ + + public GhidraValuesMap askValues(String title, String optionalMessage, GhidraValuesMap values) + throws CancelledException { + for (AbstractValue value : values.getValues()) { + String key = join(title, value.getName()); + loadAskValue(value.getValue(), s -> value.setAsText(s), key); + } + if (isRunningHeadless()) { + return values; + } + String key = generateKey(values); + return doAsk(GValuesMap.class, title, key, values, v -> { + if (v != values) { + values.copyValues(v); + } + ValuesMapDialog dialog = new ValuesMapDialog(title, optionalMessage, values); + DockingWindowManager.showDialog(dialog); + if (dialog.isCancelled()) { + throw new CancelledException(); + } + return (GhidraValuesMap) dialog.getValues(); + }); + } + + /** + * Generates a string key unique to the values defined in the given ValuesMap. Used to store + * and load previously chosen values for the given values map. + * @param valuesMap the ValuesMap to generate a key for + * @return a string that is unique for the values defined in the given ValuesMap + */ + private String generateKey(GValuesMap valuesMap) { + return valuesMap.getValues().stream().map(v -> v.getName()).collect(Collectors.joining()); + } + /** * Returns an int, using the String parameters for guidance. The actual behavior of the * method depends on your environment, which can be GUI or headless. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraState.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraState.java index 1fff5f7b64..e270e1fa1d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraState.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraState.java @@ -18,8 +18,6 @@ package ghidra.app.script; import java.util.HashMap; import java.util.Set; -import javax.swing.JOptionPane; - import ghidra.app.events.*; import ghidra.framework.model.Project; import ghidra.framework.plugintool.PluginEvent; @@ -29,7 +27,6 @@ import ghidra.program.model.address.AddressSet; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; import ghidra.program.util.ProgramSelection; -import ghidra.util.Swing; import ghidra.util.SystemUtilities; /** @@ -42,7 +39,6 @@ public class GhidraState { private ProgramSelection currentSelection; private ProgramSelection currentHighlight; private HashMap envmap = new HashMap<>(); - private GatherParamPanel gatherParamPanel; private Project project; private final boolean isGlobalState; @@ -64,11 +60,6 @@ public class GhidraState { this.currentSelection = selection; this.currentHighlight = highlight; this.isGlobalState = true; - if (!SystemUtilities.isInHeadlessMode()) { - Swing.runNow(() -> { - gatherParamPanel = new GatherParamPanel(this); - }); - } } public GhidraState(GhidraState state) { @@ -115,10 +106,6 @@ public class GhidraState { return; } this.currentProgram = program; - if (gatherParamPanel == null) { - return; - } - gatherParamPanel.currentProgramChanged(); } /** @@ -252,34 +239,6 @@ public class GhidraState { return envmap.get(name); } - public void addParameter(String key, String label, int type, Object defaultValue) { - if (gatherParamPanel == null) { - return; - } - gatherParamPanel.addParameter(key, label, type, defaultValue); - } - - public boolean displayParameterGatherer(String title) { - if (gatherParamPanel == null) { - return false; - } - if (!gatherParamPanel.panelShown()) { - int ans = JOptionPane.showConfirmDialog(null, gatherParamPanel, title, - JOptionPane.OK_CANCEL_OPTION); - if (ans == JOptionPane.CANCEL_OPTION) { - gatherParamPanel.setShown(false); - return false; - } - gatherParamPanel.setShown(true); - gatherParamPanel.setParamsInState(); - } - return true; - } - - public GatherParamPanel getParamPanel() { - return gatherParamPanel; - } - public Set getEnvironmentNames() { return envmap.keySet(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/SelectLanguageDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/SelectLanguageDialog.java index 499abdb4b9..23f2aa0edf 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/SelectLanguageDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/SelectLanguageDialog.java @@ -63,7 +63,7 @@ public class SelectLanguageDialog extends DialogComponentProvider { wasCancelled = true; } - boolean wasCancelled() { + public boolean wasCancelled() { return wasCancelled; } @@ -71,7 +71,7 @@ public class SelectLanguageDialog extends DialogComponentProvider { return languagePanel.getSelectedLcsPair() != null; } - void setSelectedLanguage(LanguageCompilerSpecPair language) { + public void setSelectedLanguage(LanguageCompilerSpecPair language) { Swing.runNow(() -> languagePanel.setSelectedLcsPair(language)); } 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 5b0c22d436..aa61dab27a 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 @@ -122,6 +122,7 @@ public class AddressInput extends JPanel implements FocusableEditor { /** * Returns the address in the field or null if the address can't * be parsed. + * @return The address for the current value in the text field * * @throws NullPointerException if AddressFactory has not been set. */ @@ -160,6 +161,14 @@ public class AddressInput extends JPanel implements FocusableEditor { return textField.getText().length() != 0; } + /** + * Returns the text in this field. + * @return the text in this field + */ + public String getText() { + return textField.getText(); + } + public AddressFactory getAddressFactory() { return addrFactory; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/AddressValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/AddressValue.java new file mode 100644 index 0000000000..e180fb3c3f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/AddressValue.java @@ -0,0 +1,104 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import javax.swing.JComponent; + +import docking.widgets.values.*; +import ghidra.app.util.AddressInput; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressFactory; +import ghidra.program.model.listing.Program; + +/** + * Value class for {@link Address} types. In order to parse and create Address types, an + * {@link AddressFactory} is required when defining this type. As a convenience, it can + * be constructed with a {@link Program}, in which case it will use the AddressFactory from + * that program. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class AddressValue extends AbstractValue

{ + + private AddressInput field; + private AddressFactory addressFactory; + + /** + * Creates an AddressValue with an optional default value and uses the {@link AddressFactory} + * from the given program. + * @param name the name of this value + * @param defaultValue an optional default value + * @param program the program whose AddressFactory will be used to create Addresses. + */ + public AddressValue(String name, Address defaultValue, Program program) { + this(name, defaultValue, program.getAddressFactory()); + } + + /** + * Creates an AddressValue with an optional default value. + * @param name the name of this value + * @param defaultValue an optional default value + * @param factory the AddressFactory that will be used to create Addresses. + */ + public AddressValue(String name, Address defaultValue, AddressFactory factory) { + super(name, defaultValue); + this.addressFactory = factory; + } + + @Override + public JComponent getComponent() { + if (field == null) { + field = new AddressInput(); + field.setAddressFactory(addressFactory); + } + return field; + } + + @Override + protected void updateValueFromComponent() throws ValuesMapParseException { + Address address = field.getAddress(); + if (address == null && field.hasInput()) { + throw new ValuesMapParseException(getName(), "Address", + "Could not parse \"" + field.getText() + "\"."); + } + setValue(address); + } + + @Override + protected void updateComponentFromValue() { + Address v = getValue(); + if (v == null) { + field.clear(); + } + else { + field.setAddress(v); + } + } + + @Override + protected Address fromString(String valueString) { + Address address = addressFactory.getAddress(valueString); + if (address == null) { + throw new IllegalArgumentException("Invalid address string: " + valueString); + } + return address; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java new file mode 100644 index 0000000000..6144698b86 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java @@ -0,0 +1,288 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import docking.Tool; +import docking.widgets.values.GValuesMap; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressFactory; +import ghidra.program.model.lang.LanguageCompilerSpecPair; +import ghidra.program.model.listing.Program; + +/** + * Extends GValuesMap to add Ghidra specific types such as Address and Program + */ +public class GhidraValuesMap extends GValuesMap { + +//================================================================================================== +// Define Value Methods +//================================================================================================== + + /** + * Defines a value of type {@link Address} with no default value. + * @param name the name for this value + * @param program the program used to get an {@link AddressFactory} for parsing addresses + * @return the new AddressValue that was defined. + */ + public AddressValue defineAddress(String name, Program program) { + checkDup(name); + AddressValue value = new AddressValue(name, null, program); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type {@link Address} + * @param name the name for this value + * @param defaultValue an option default value + * @param program the program used to get an {@link AddressFactory} for parsing addresses + * @return the new AddressValue that was defined. + */ + public AddressValue defineAddress(String name, Address defaultValue, Program program) { + checkDup(name); + AddressValue value = new AddressValue(name, defaultValue, program); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type {@link Address} + * @param name the name for this value + * @param defaultValue an option default value + * @param factory the {@link AddressFactory} used to parse addresses + * @return the new AddressValue that was defined. + */ + public AddressValue defineAddress(String name, Address defaultValue, AddressFactory factory) { + checkDup(name); + AddressValue value = new AddressValue(name, defaultValue, factory); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type LanguageCompilerSpecPair (folders in a Ghidra project). + * @param name the name for this value + * @param defaultValue the initial value (can be null) + * @return the new ProjectFolderValue that was defined + */ + public LanguageValue defineLanguage(String name, LanguageCompilerSpecPair defaultValue) { + checkDup(name); + LanguageValue value = new LanguageValue(name, defaultValue); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Program. This method opens programs using the given + * consumer and must be properly released when it is no longer needed. This is true + * even if the program is also opened in the tool. + * @param name the name for this value + * @param consumer the consumer to be used to open the program + * @param tool if non-null, the program will also be opened in the given tool + * @return the user-selected Program if a program was + * not selected or null. NOTE: It is very important that the program instance + * returned by this method ALWAYS be properly released from the consumer when no + * longer needed (i.e., {@code program.release(consumer) } - failure to + * properly release the program may result in improper project disposal. If the program was + * also opened in the tool, the tool will be a second consumer responsible for its + * own release. + */ + public ProgramValue defineProgram(String name, Object consumer, Tool tool) { + return defineProgram(name, null, consumer, tool); + } + + /** + * Defines a value of type Program. + * @param name the name for this value + * @param defaultValue the initial value + * @param consumer the consumer to be used to open the program + * @param tool if non-null, the program will also be opened in the given tool + * @return the new ProgramValue that was defined + */ + public ProgramValue defineProgram(String name, Program defaultValue, Object consumer, + Tool tool) { + checkDup(name); + ProgramValue value = new ProgramValue(name, defaultValue, consumer, tool); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type DomainFile (files in a Ghidra project). + * @param name the name for this value + * @return the new ProjectFileValue that was defined + */ + public ProjectFileValue defineProjectFile(String name) { + return defineProjectFile(name, null); + } + + /** + * Defines a value of type DomainFile (files in a Ghidra project). + * @param name the name for this value + * @param defaultValue the initial value + * @return the new ProjectFileValue that was defined + */ + public ProjectFileValue defineProjectFile(String name, DomainFile defaultValue) { + checkDup(name); + ProjectFileValue value = new ProjectFileValue(name, defaultValue); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type DomainFolder (folders in a Ghidra project). + * @param name the name for this value + * @return the new ProjectFolderValue that was defined + */ + public ProjectFolderValue defineProjectFolder(String name) { + return defineProjectFolder(name, null); + } + + /** + * Defines a value of type DomainFolder (files in a Ghidra project). + * @param name the name for this value + * @param defaultValue the initial value (can be null) + * @return the new ProjectFolderValue that was defined + */ + public ProjectFolderValue defineProjectFolder(String name, DomainFolder defaultValue) { + checkDup(name); + ProjectFolderValue value = new ProjectFolderValue(name, defaultValue); + valuesMap.put(name, value); + return value; + } + + /** + * Gets the {@link Address} value for the given name. + * @param name the name of a previously defined Address value + * @return the Address + * @throws IllegalArgumentException if the name hasn't been defined as an Address type + */ + public Address getAddress(String name) { + AddressValue addressValue = getValue(name, AddressValue.class, "Address"); + return addressValue.getValue(); + } + +//================================================================================================== +// Get Value Methods +//================================================================================================== + /** + * Gets the Language ({@link LanguageCompilerSpecPair}) value for the given name. + * @param name the name of a previously defined language value + * @return the language value + * @throws IllegalArgumentException if the name hasn't been defined as a language type + */ + public LanguageCompilerSpecPair getLanguage(String name) { + LanguageValue value = getValue(name, LanguageValue.class, "Language"); + return value.getValue(); + } + + /** + * Gets the {@link Program} value for the given name. + * @param name the name of a previously defined project folder value + * @return the project folder value + * @throws IllegalArgumentException if the name hasn't been defined as a project folder type + */ + public Program getProgram(String name) { + ProgramValue programValue = getValue(name, ProgramValue.class, "Program"); + return programValue.getValue(); + } + + /** + * Gets the project file ({@link DomainFile}) value for the given name. + * @param name the name of a previously defined project file value + * @return the project file value + * @throws IllegalArgumentException if the name hasn't been defined as a project file type + */ + public DomainFile getProjectFile(String name) { + ProjectFileValue domainFileValue = getValue(name, ProjectFileValue.class, "Domain File"); + return domainFileValue.getValue(); + } + + /** + * Gets the project folder ({@link DomainFolder}) value for the given name. + * @param name the name of a previously defined project folder value + * @return the project folder value + * @throws IllegalArgumentException if the name hasn't been defined as a project folder type + */ + public DomainFolder getProjectFolder(String name) { + ProjectFolderValue domainFolderValue = + getValue(name, ProjectFolderValue.class, "Domain Folder"); + return domainFolderValue.getValue(); + } + +//================================================================================================== +// Set Value Methods +//================================================================================================== + + /** + * Sets the address value for the given name. + * @param name the name of the Address value that was previously defined + * @param address the address to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as an Address type + */ + public void setAddress(String name, Address address) { + AddressValue addressValue = getValue(name, AddressValue.class, "Address"); + addressValue.setValue(address); + } + + /** + * Sets the Language ({@link LanguageCompilerSpecPair}) value for the given name. + * @param name the name of the Language value that was previously defined + * @param value the Language to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as a Language type + */ + public void setLanguage(String name, LanguageCompilerSpecPair value) { + LanguageValue languageValue = getValue(name, LanguageValue.class, "Language"); + languageValue.setValue(value); + } + + /** + * Sets the {@link Program} value for the given name. + * @param name the name of the Program value that was previously defined + * @param program the Program to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as a Program type + */ + public void setProgram(String name, Program program) { + ProgramValue programValue = getValue(name, ProgramValue.class, "Program"); + programValue.setValue(program); + } + + /** + * Sets the project file {@link DomainFile} value for the given name. + * @param name the name of the project file value that was previously defined + * @param file the project file to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as a project file type + */ + public void setProjectFile(String name, DomainFile file) { + ProjectFileValue domainFileValue = getValue(name, ProjectFileValue.class, "Domain File"); + domainFileValue.setValue(file); + } + + /** + * Sets the project folder {@link DomainFolder} value for the given name. + * @param name the name of the project folder value that was previously defined + * @param folder the project folder to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as a project folder type + */ + public void setProjectFolder(String name, DomainFolder folder) { + ProjectFolderValue domainFolderValue = + getValue(name, ProjectFolderValue.class, "Domain Folder"); + domainFolderValue.setValue(folder); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/LanguageValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/LanguageValue.java new file mode 100644 index 0000000000..df84b283b0 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/LanguageValue.java @@ -0,0 +1,195 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import java.awt.BorderLayout; +import java.io.File; +import java.util.*; + +import javax.swing.*; + +import docking.widgets.button.BrowseButton; +import docking.widgets.values.*; +import ghidra.app.script.SelectLanguageDialog; +import ghidra.program.model.lang.*; +import ghidra.program.util.DefaultLanguageService; + +/** + * Value class for LanguageCompilerSpecPair types. The component for this class is a + * TextField with a browse button for bringing up a language/compiler chooser. It supports + * the concept of no value when the text field is empty. If it is not empty, the the contents + * must be one of the known valid language/compiler spec pairs. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class LanguageValue extends AbstractValue { + private LangaugeValuePanel languagePanel; + + /** + * Construct a new LanguageVlue with no value + * @param name the name of the value + */ + public LanguageValue(String name) { + super(name, null); + } + + /** + * Construct a new LanguageVlue with a given optional default value. + * @param name the name of the value + * @param defaultValue the optional default value + */ + public LanguageValue(String name, LanguageCompilerSpecPair defaultValue) { + super(name, defaultValue); + } + + @Override + public JComponent getComponent() { + if (languagePanel == null) { + languagePanel = new LangaugeValuePanel(getName()); + } + return languagePanel; + } + + @Override + protected void updateValueFromComponent() throws ValuesMapParseException { + setValue(languagePanel.getLanguage()); + } + + @Override + protected void updateComponentFromValue() { + languagePanel.setLanguage(getValue()); + } + + @Override + public LanguageCompilerSpecPair fromString(String valueString) { + try { + return parseLanguageCompileSpecPair(valueString); + } + catch (ValuesMapParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + /** + * Parses a LanguageCompilerSpecPair from a string. + * + * @param val The string to parse. + * @return The LanguageCompilerSpecPair parsed from a string or null if the string does + * not parse to a known language-compiler pair. + * @throws ValuesMapParseException + */ + public LanguageCompilerSpecPair parseLanguageCompileSpecPair(String val) + throws ValuesMapParseException { + + if (val.isBlank()) { + return null; + } + // Split on last colon to get separated languageID and compilerSpecID + int lastColon = val.lastIndexOf(':'); + if (lastColon < 1) { + throw new ValuesMapParseException(getName(), "Language/Compiler Spec", + "Could not parse \"" + val + "\"."); + } + + Set languages = getLanguagesCompilerPairs(); + + String langId = val.substring(0, lastColon); + String compilerId = val.substring(lastColon + 1); + + LanguageCompilerSpecPair storedLCS = new LanguageCompilerSpecPair(langId, compilerId); + if (!languages.contains(storedLCS)) { + throw new ValuesMapParseException(getName(), "Language/Compiler Spec", + "Unknown language/Compiler Pair for \"" + val + "\""); + } + return storedLCS; + } + + private Set getLanguagesCompilerPairs() { + Set languages = new HashSet<>(); + LanguageService languageService = DefaultLanguageService.getLanguageService(); + List descriptions = languageService.getLanguageDescriptions(false); + for (LanguageDescription description : descriptions) { + Collection csDescriptions = + description.getCompatibleCompilerSpecDescriptions(); + for (CompilerSpecDescription csDescription : csDescriptions) { + languages.add(new LanguageCompilerSpecPair(description.getLanguageID(), + csDescription.getCompilerSpecID())); + } + } + return languages; + } + + class LangaugeValuePanel extends JPanel { + private JTextField textField; + private JButton browseButton; + + public LangaugeValuePanel(String name) { + super(new BorderLayout()); + setName(name); + textField = new JTextField(20); + browseButton = new BrowseButton(); + browseButton.addActionListener(e -> showLanguageDialog()); + add(textField, BorderLayout.CENTER); + add(browseButton, BorderLayout.EAST); + } + + public LanguageCompilerSpecPair getLanguage() throws ValuesMapParseException { + return parseLanguageCompileSpecPair(textField.getText()); + } + + public void setLanguage(LanguageCompilerSpecPair value) { + String text = value == null ? "" : value.toString(); + textField.setText(text); + } + + private void showLanguageDialog() { + SelectLanguageDialog dialog = new SelectLanguageDialog("Select Language", "Ok"); + + try { + dialog.setSelectedLanguage(getLanguage()); + } + catch (ValuesMapParseException e) { + // we are just trying to initialize dialog, so don't care at this time + } + dialog.show(); + + LanguageCompilerSpecPair selectedLanguage = dialog.getSelectedLanguage(); + + if (selectedLanguage != null) { + textField.setText(selectedLanguage.toString()); + } + dialog.dispose(); + } + + public File getFile() { + String text = textField.getText().trim(); + if (text.isBlank()) { + return null; + } + return new File(text); + } + + public void setText(String val) { + textField.setText(val); + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramValue.java new file mode 100644 index 0000000000..d5ef2e5e18 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramValue.java @@ -0,0 +1,153 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import java.io.IOException; + +import javax.swing.JComponent; +import javax.swing.JTextField; + +import docking.Tool; +import docking.widgets.values.*; +import ghidra.app.services.ProgramManager; +import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainObject; +import ghidra.program.model.listing.Program; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.VersionException; +import ghidra.util.task.TaskMonitor; + +/** + * Value class for {@link Program}s. The editor component consists of the {@link JTextField} and + * a browse button for bringing up a {@link DataTreeDialog} for picking programs from the + * current project. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class ProgramValue extends AbstractValue { + + private ProjectBrowserPanel domainFilePanel; + private Tool tool; + private Object consumer; + + /** + * Construct for ProgramValue + * @param name the name of the value + * @param consumer the program consumer to be used to open a program + * @param tool if non null, the program will also be opened in this tool + */ + public ProgramValue(String name, Object consumer, Tool tool) { + this(name, null, consumer, tool); + } + + /** + * Construct for ProgramValue + * @param name the name of the value + * @param defaultValue the program to use as the default value + * @param consumer the program consumer to be used to open a program + * @param tool if non null, the program will also be opened in this tool + */ + public ProgramValue(String name, Program defaultValue, Object consumer, Tool tool) { + super(name, defaultValue); + this.consumer = consumer; + this.tool = tool; + } + + @Override + public JComponent getComponent() { + if (domainFilePanel == null) { + domainFilePanel = new ProjectBrowserPanel(getName(), false); + } + return domainFilePanel; + } + + @Override + protected void updateValueFromComponent() throws ValuesMapParseException { + if (domainFilePanel != null) { + DomainFile domainFile = domainFilePanel.getDomainFile(); + if (domainFile == null) { + String text = domainFilePanel.getText(); + if (text.isBlank()) { + setValue(null); + return; + } + throw new ValuesMapParseException(getName(), "Program", + "No file found for \"" + text + "\""); + } + Program program = openProgram(domainFile); + setValue(program); + } + } + + private Program openProgram(DomainFile domainFile) throws ValuesMapParseException { + if (domainFile == null) { + return null; + } + Class domainObjectClass = domainFile.getDomainObjectClass(); + if (!Program.class.isAssignableFrom(domainObjectClass)) { + return null; + } + try { + Program program = + (Program) domainFile.getDomainObject(consumer, false, false, TaskMonitor.DUMMY); + + if (tool != null && program != null) { + tool.getService(ProgramManager.class).openProgram(program); + } + return program; + } + catch (VersionException | CancelledException | IOException e) { + throw new ValuesMapParseException(getName(), "Program", e.getMessage()); + } + } + + @Override + protected void updateComponentFromValue() { + Program program = getValue(); + DomainFile df = program == null ? null : program.getDomainFile(); + domainFilePanel.setDomainFile(df); + } + + @Override + protected Program fromString(String valueString) { + DomainFile programFile = ProjectBrowserPanel.parseDomainFile(valueString); + if (programFile == null) { + throw new IllegalArgumentException("Could not find program " + valueString); + } + try { + Program program = openProgram(programFile); + if (program == null) { + throw new IllegalArgumentException("Can't open program: " + valueString); + } + return program; + } + catch (ValuesMapParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + @Override + protected String toString(Program v) { + return v.getDomainFile().getPathname(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectBrowserPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectBrowserPanel.java new file mode 100644 index 0000000000..4975c71919 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectBrowserPanel.java @@ -0,0 +1,117 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import java.awt.BorderLayout; + +import javax.swing.*; + +import docking.widgets.button.BrowseButton; +import ghidra.framework.main.AppInfo; +import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.model.*; +import ghidra.framework.store.FileSystem; + +/** + * Component used by Values that use the DataTreeDialog for picking DomainFiles and DomainFolders + */ +class ProjectBrowserPanel extends JPanel { + private JTextField textField; + private JButton browseButton; + private boolean selectFolders; + + ProjectBrowserPanel(String name, boolean selectFolders) { + super(new BorderLayout()); + this.selectFolders = selectFolders; + setName(name); + textField = new JTextField(20); + browseButton = new BrowseButton(); + browseButton.addActionListener(e -> showDomainFileChooser()); + add(textField, BorderLayout.CENTER); + add(browseButton, BorderLayout.EAST); + } + + void setDomainFile(DomainFile value) { + String text = value == null ? "" : value.getPathname(); + textField.setText(text); + } + + void setDomainFolder(DomainFolder value) { + String text = value == null ? "" : value.getPathname(); + textField.setText(text); + } + + private void showDomainFileChooser() { + DataTreeDialog dialog = new DataTreeDialog(null, "Choose " + getName(), + selectFolders ? DataTreeDialog.CHOOSE_FOLDER : DataTreeDialog.OPEN); + dialog.show(); + if (dialog.wasCancelled()) { + return; + } + String text = selectFolders ? dialog.getDomainFolder().getPathname() + : dialog.getDomainFile().getPathname(); + textField.setText(text); + dialog.dispose(); + } + + DomainFile getDomainFile() { + String text = textField.getText().trim(); + if (text.isBlank()) { + return null; + } + return parseDomainFile(text); + } + + String getText() { + return textField.getText().trim(); + } + + DomainFolder getDomainFolder() { + String text = textField.getText().trim(); + if (text.isBlank()) { + return parseDomainFolder("/"); + } + return parseDomainFolder(text); + } + + static DomainFile parseDomainFile(String val) { + // Add the slash to make it an absolute path + if (!val.isEmpty() && val.charAt(0) != FileSystem.SEPARATOR_CHAR) { + val = FileSystem.SEPARATOR_CHAR + val; + } + Project activeProject = AppInfo.getActiveProject(); + DomainFile df = activeProject.getProjectData().getFile(val); + if (df != null) { + return df; + } + return null; + } + + static DomainFolder parseDomainFolder(String path) { + path = path.trim(); + // Add the slash to make it an absolute path + if (path.isEmpty() || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { + path = FileSystem.SEPARATOR_CHAR + path; + } + Project activeProject = AppInfo.getActiveProject(); + DomainFolder df = activeProject.getProjectData().getFolder(path); + if (df != null) { + return df; + } + return null; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFileValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFileValue.java new file mode 100644 index 0000000000..7af04f7690 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFileValue.java @@ -0,0 +1,113 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import javax.swing.JComponent; +import javax.swing.JTextField; + +import docking.widgets.values.*; +import ghidra.framework.main.AppInfo; +import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.Project; +import ghidra.framework.store.FileSystem; + +/** + * Value class for project files ({@link DomainFile}). The editor component consists of the + * {@link JTextField} and a browse button for bringing up a {@link DataTreeDialog} for picking + * project files from the current project. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class ProjectFileValue extends AbstractValue { + + private ProjectBrowserPanel domainFilePanel; + + public ProjectFileValue(String name) { + this(name, null); + } + + public ProjectFileValue(String name, DomainFile defaultValue) { + super(name, defaultValue); + } + + @Override + public JComponent getComponent() { + if (domainFilePanel == null) { + domainFilePanel = new ProjectBrowserPanel(getName(), false); + } + return domainFilePanel; + } + + @Override + protected void updateValueFromComponent() throws ValuesMapParseException { + if (domainFilePanel != null) { + DomainFile domainFile = domainFilePanel.getDomainFile(); + if (domainFile == null) { + String text = domainFilePanel.getText(); + if (text.isBlank()) { + setValue(null); + return; + } + throw new ValuesMapParseException(getName(), "Project File", + "No file found for \"" + text + "\""); + } + setValue(domainFile); + } + } + + @Override + protected void updateComponentFromValue() { + if (domainFilePanel != null) { + domainFilePanel.setDomainFile(getValue()); + } + } + + @Override + protected DomainFile fromString(String valueString) { + DomainFile df = parseDomainFile(valueString); + if (df == null) { + throw new IllegalArgumentException("Can't find domain file: " + valueString); + } + return df; + } + + private DomainFile parseDomainFile(String val) { + // Add the slash to make it an absolute path + if (!val.isEmpty() && val.charAt(0) != FileSystem.SEPARATOR_CHAR) { + val = FileSystem.SEPARATOR_CHAR + val; + } + Project activeProject = AppInfo.getActiveProject(); + if (activeProject == null) { + throw new IllegalStateException("No Active Project!"); + } + DomainFile df = activeProject.getProjectData().getFile(val); + if (df != null) { + return df; + } + return null; + } + + @Override + protected String toString(DomainFile v) { + return v.getPathname(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFolderValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFolderValue.java new file mode 100644 index 0000000000..742a284b61 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFolderValue.java @@ -0,0 +1,98 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import javax.swing.JComponent; +import javax.swing.JTextField; + +import docking.widgets.values.*; +import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; + +/** + * Value class for project folders ({@link DomainFile}). The editor component consists of the + * {@link JTextField} and a browse button for bringing up a {@link DataTreeDialog} for picking + * project folders from the current project. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class ProjectFolderValue extends AbstractValue { + + private ProjectBrowserPanel domainFilePanel; + + public ProjectFolderValue(String name) { + this(name, null); + } + + public ProjectFolderValue(String name, DomainFolder defaultValue) { + super(name, defaultValue); + } + + @Override + public JComponent getComponent() { + if (domainFilePanel == null) { + domainFilePanel = new ProjectBrowserPanel(getName(), true); + } + return domainFilePanel; + } + + @Override + protected void updateValueFromComponent() throws ValuesMapParseException { + if (domainFilePanel != null) { + DomainFolder domainFolder = domainFilePanel.getDomainFolder(); + if (domainFolder == null) { + String text = domainFilePanel.getText(); + if (text.isBlank()) { + setValue(null); + return; + } + throw new ValuesMapParseException(getName(), "Project Folder", + "No folder found for \"" + text + "\""); + } + setValue(domainFolder); + } + } + + @Override + protected void updateComponentFromValue() { + + if (domainFilePanel != null) { + domainFilePanel.setDomainFolder(getValue()); + + } + } + + @Override + protected DomainFolder fromString(String valueString) { + DomainFolder df = ProjectBrowserPanel.parseDomainFolder(valueString); + if (df == null) { + throw new IllegalArgumentException("Can't find domain folder: " + valueString); + } + return df; + } + + @Override + protected String toString(DomainFolder v) { + return v.getPathname(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java index 0ce5d495ce..f798c1b33a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java @@ -435,6 +435,14 @@ public class DataTreeDialog extends DialogComponentProvider Swing.runLater(() -> treePanel.selectRootDataFolder()); } + /** + * Select a folder in the tree. + * @param folder the folder to select + */ + public void selectFolder(DomainFolder folder) { + Swing.runLater(() -> treePanel.selectDomainFolder(folder)); + } + /** * Select the node that corresponds to the given domain file. * @param file the file diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/NewLanguagePanel.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/NewLanguagePanel.java index ea028ed21b..2322163d2d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/NewLanguagePanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/NewLanguagePanel.java @@ -299,6 +299,9 @@ public class NewLanguagePanel extends JPanel { } public boolean setSelectedLcsPair(LanguageCompilerSpecPair lcsPair) { + if (lcsPair == null) { + return false; + } int index = tableModel.getFirstLcsPairIndex(lcsPair); if (index == -1) { return false; diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/AbstractValueIntegrationTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/AbstractValueIntegrationTest.java new file mode 100644 index 0000000000..04ef8ef681 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/AbstractValueIntegrationTest.java @@ -0,0 +1,132 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import javax.swing.JButton; +import javax.swing.JTextField; + +import org.junit.After; +import org.junit.Before; + +import docking.DialogComponentProvider; +import docking.DockingWindowManager; +import docking.widgets.values.AbstractValue; +import docking.widgets.values.ValuesMapDialog; +import ghidra.features.base.values.GhidraValuesMap; +import ghidra.features.base.values.ProjectBrowserPanel; +import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.model.*; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.database.ProgramDB; +import ghidra.program.model.address.*; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.test.TestEnv; +import ghidra.util.task.TaskMonitor; + +public abstract class AbstractValueIntegrationTest extends AbstractGhidraHeadedIntegrationTest { + + protected ValuesMapDialog dialog; + protected GhidraValuesMap values = new GhidraValuesMap(); + + protected TestEnv env; + protected DomainFolder rootFolder; + protected DomainFolder folder; + protected DomainFile fileA; + protected DomainFile fileB; + protected ProgramDB programA; + protected ProgramDB programB; + + @Before + public void setup() throws Exception { + env = new TestEnv(); + Project project = env.getProject(); + runSwing(() -> env.getFrontEndTool().setActiveProject(project)); + rootFolder = project.getProjectData().getRootFolder(); + folder = rootFolder.createFolder("A"); + ProgramBuilder programBuilderA = new ProgramBuilder("A", ProgramBuilder._TOY); + ProgramBuilder programBuilderB = new ProgramBuilder("B", ProgramBuilder._TOY); + programA = programBuilderA.getProgram(); + programB = programBuilderB.getProgram(); + fileA = folder.createFile("A", programA, TaskMonitor.DUMMY); + fileB = folder.createFile("B", programB, TaskMonitor.DUMMY); + + } + + @After + public void tearDown() { + env.dispose(); + } + + protected void showDialogOnSwingWithoutBlocking() { + + runSwing(() -> { + dialog = new ValuesMapDialog("Test", null, values); + DockingWindowManager.showDialog(dialog); + }, false); + + waitForDialogComponent(DialogComponentProvider.class); + } + + protected void pressOk() { + JButton okButton = (JButton) getInstanceField("okButton", dialog); + runSwing(() -> okButton.doClick()); + } + + protected void pressCancel() { + JButton okButton = (JButton) getInstanceField("cancelButton", dialog); + runSwing(() -> okButton.doClick()); + } + + protected void setProjectFileOnProjectTree(AbstractValue value, DomainFile file) { + ProjectBrowserPanel projectWidget = (ProjectBrowserPanel) value.getComponent(); + pressButtonByName(projectWidget, "BrowseButton", false); + + DataTreeDialog dataTreeDialog = waitForDialogComponent(DataTreeDialog.class); + runSwing(() -> { + dataTreeDialog.selectDomainFile(file); + }); + waitForSwing(); + pressButtonByText(dataTreeDialog, "OK"); + + } + + protected void setProjectFolderOnProjectTree(AbstractValue value, DomainFolder folder) { + ProjectBrowserPanel projectWidget = (ProjectBrowserPanel) value.getComponent(); + pressButtonByName(projectWidget, "BrowseButton", false); + + DataTreeDialog dataTreeDialog = waitForDialogComponent(DataTreeDialog.class); + runSwing(() -> { + dataTreeDialog.selectFolder(folder); + }); + waitForSwing(); + pressButtonByText(dataTreeDialog, "OK"); + + } + + protected void setTextOnComponent(AbstractValue nameValue, String text) { + runSwing(() -> { + JTextField field = (JTextField) nameValue.getComponent(); + field.setText(text); + }); + } + + protected AddressFactory createAddressFactory() { + GenericAddressSpace space1 = new GenericAddressSpace("A", 64, AddressSpace.TYPE_RAM, 0); + GenericAddressSpace space2 = new GenericAddressSpace("B", 64, AddressSpace.TYPE_RAM, 0); + return new DefaultAddressFactory(new AddressSpace[] { space1, space2 }); + } + +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/AddressValueTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/AddressValueTest.java new file mode 100644 index 0000000000..7ad096c428 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/AddressValueTest.java @@ -0,0 +1,150 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import docking.widgets.values.AbstractValue; +import ghidra.app.util.AddressInput; +import ghidra.features.base.values.AddressValue; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressFactory; + +public class AddressValueTest extends AbstractValueIntegrationTest { + private static final String NAME = "Start Address"; + protected AddressFactory factory = createAddressFactory(); + + @Test + public void testAddressValueNoDefault() { + values.defineAddress(NAME, null, factory); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + + values.setAddress(NAME, addr(13)); + assertTrue(values.hasValue(NAME)); + + assertEquals(addr(13), values.getAddress(NAME)); + } + + @Test + public void testAddressValueWithDefault() { + values.defineAddress(NAME, addr(1), factory); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(addr(1), values.getAddress(NAME)); + + values.setAddress(NAME, addr(2)); + assertTrue(values.hasValue(NAME)); + + assertEquals(addr(2), values.getAddress(NAME)); + + values.setAddress(NAME, null); + assertFalse(values.hasValue(NAME)); + } + + @Test + public void testGetAsText() { + AddressValue value1 = new AddressValue(NAME, addr(0x123), factory); + AddressValue value2 = new AddressValue(NAME, null, factory); + assertEquals("A:00000123", value1.getAsText()); + assertNull(value2.getAsText()); + } + + @Test + public void testSetAsText() { + AddressValue v = new AddressValue(NAME, null, factory); + assertEquals(addr(0x123), v.setAsText("A:00000123")); + try { + v.setAsText("xdsf"); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + try { + v.setAsText(null); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineAddress(NAME, null, factory); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertFalse(values.hasValue(NAME)); + assertNull(values.getAddress(NAME)); + } + + @Test + public void testNoDefaultValueWithDialogInput() { + values.defineAddress(NAME, null, factory); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setTextOnAddressInput(values.getAbstractValue(NAME), "2"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(addr(2), values.getAddress(NAME)); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineAddress(NAME, addr(1), factory); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(addr(1), values.getAddress(NAME)); + } + + @Test + public void testDefaultValueWithDialogInput() { + values.defineAddress(NAME, addr(1), factory); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setTextOnAddressInput(values.getAbstractValue(NAME), "2"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(addr(2), values.getAddress(NAME)); + } + + private Address addr(int offset) { + return factory.getDefaultAddressSpace().getAddress(offset); + } + + protected void setTextOnAddressInput(AbstractValue nameValue, String text) { + runSwing(() -> { + AddressInput addressInput = (AddressInput) nameValue.getComponent(); + addressInput.setValue(text); + }); + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/LanguageValueTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/LanguageValueTest.java new file mode 100644 index 0000000000..0268fe68fa --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/LanguageValueTest.java @@ -0,0 +1,167 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import docking.widgets.values.AbstractValue; +import ghidra.app.script.SelectLanguageDialog; +import ghidra.features.base.values.LanguageValue; +import ghidra.features.base.values.LanguageValue.LangaugeValuePanel; +import ghidra.program.model.lang.LanguageCompilerSpecPair; + +public class LanguageValueTest extends AbstractValueIntegrationTest { + private static final String NAME = "Lang"; + private static final LanguageCompilerSpecPair LANG1 = + new LanguageCompilerSpecPair("6502:LE:16:default", "default"); + private static final LanguageCompilerSpecPair LANG2 = + new LanguageCompilerSpecPair("ARM:BE:32:v7", "default"); + + @Test + public void testLanguageValueNoDefault() { + values.defineLanguage(NAME, null); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + + values.setLanguage(NAME, LANG1); + assertTrue(values.hasValue(NAME)); + + assertEquals(LANG1, values.getLanguage(NAME)); + } + + @Test + public void testLanguageValueWithDefault() { + values.defineLanguage(NAME, LANG1); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(LANG1, values.getLanguage(NAME)); + + values.setLanguage(NAME, LANG2); + assertTrue(values.hasValue(NAME)); + + assertEquals(LANG2, values.getLanguage(NAME)); + + values.setLanguage(NAME, null); + assertFalse(values.hasValue(NAME)); + } + + @Test + public void testGetAsText() { + LanguageValue value1 = new LanguageValue(NAME); + LanguageValue value2 = new LanguageValue(NAME, LANG1); + assertNull(value1.getAsText()); + assertEquals("6502:LE:16:default:default", value2.getAsText()); + } + + @Test + public void testSetAsText() { + LanguageValue v = new LanguageValue(NAME); + assertEquals(LANG1, v.setAsText("6502:LE:16:default:default")); + try { + v.setAsText(null); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineLanguage(NAME, null); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertFalse(values.hasValue(NAME)); + assertNull(values.getLanguage(NAME)); + } + + @Test + public void testNoDefaultValueWithDialogInput() { + values.defineLanguage(NAME, null); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setLanguage(values.getAbstractValue(NAME), LANG1); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(LANG1, values.getLanguage(NAME)); + } + + @Test + public void testNoDefaultValueWithBadDialogInput() { + values.defineLanguage(NAME, null); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setLanguage(values.getAbstractValue(NAME), "asdfa"); + pressOk(); + + assertTrue(dialog.isShowing()); + pressCancel(); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineLanguage(NAME, LANG1); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(LANG1, values.getLanguage(NAME)); + } + + @Test + public void testDefaultValueWithDialogInput() { + values.defineLanguage(NAME, LANG1); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setLanguage(values.getAbstractValue(NAME), LANG2); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(LANG2, values.getLanguage(NAME)); + } + + protected void setLanguage(AbstractValue nameValue, LanguageCompilerSpecPair lang) { + LangaugeValuePanel languageWidget = (LangaugeValuePanel) nameValue.getComponent(); + pressButtonByName(languageWidget, "BrowseButton", false); + SelectLanguageDialog langDialog = waitForDialogComponent(SelectLanguageDialog.class); + runSwing(() -> { + langDialog.setSelectedLanguage(lang); + }); + + pressButtonByText(langDialog, "Ok"); + + } + + protected void setLanguage(AbstractValue nameValue, String val) { + LangaugeValuePanel languageWidget = (LangaugeValuePanel) nameValue.getComponent(); + runSwing(() -> { + languageWidget.setText(val); + }); + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramValueTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramValueTest.java new file mode 100644 index 0000000000..d78e1603d2 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramValueTest.java @@ -0,0 +1,155 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.app.services.ProgramManager; +import ghidra.features.base.values.ProgramValue; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; + +public class ProgramValueTest extends AbstractValueIntegrationTest { + private static final String NAME = "Program"; + + @Test + public void testProgramValueNoDefault() { + values.defineProgram(NAME, this, null); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + + values.setProgram(NAME, programA); + assertTrue(values.hasValue(NAME)); + + assertEquals(programA, values.getProgram(NAME)); + } + + @Test + public void testProgramValueWithDefault() { + values.defineProgram(NAME, programA, this, null); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(programA, values.getProgram(NAME)); + + values.setProgram(NAME, programB); + assertTrue(values.hasValue(NAME)); + + assertEquals(programB, values.getProgram(NAME)); + + values.setProgram(NAME, null); + assertFalse(values.hasValue(NAME)); + } + + @Test + public void testGetAsText() { + ProgramValue value1 = new ProgramValue(NAME, this, null); + ProgramValue value2 = new ProgramValue(NAME, programA, this, null); + assertNull(value1.getAsText()); + assertEquals("/A/A", value2.getAsText()); + } + + @Test + public void testSetAsText() { + ProgramValue v = new ProgramValue(NAME, this, null); + assertEquals(programA, v.setAsText("/A/A")); + try { + v.setAsText(null); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + try { + v.setAsText("/z/z/t"); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineProgram(NAME, this, null); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertFalse(values.hasValue(NAME)); + assertNull(values.getProgram(NAME)); + } + + @Test + public void testNoDefaultValueWithDialogInput() { + values.defineProgram(NAME, this, null); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setProjectFileOnProjectTree(values.getAbstractValue(NAME), programA.getDomainFile()); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(programA, values.getProgram(NAME)); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineProgram(NAME, programA, this, null); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(programA, values.getProgram(NAME)); + } + + @Test + public void testDefaultValueWithDialogInput() { + values.defineProgram(NAME, programA, this, null); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setProjectFileOnProjectTree(values.getAbstractValue(NAME), programB.getDomainFile()); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(programB, values.getProgram(NAME)); + } + + @Test + public void testOpenProgramInTool() { + PluginTool tool = env.createDefaultTool(); + ProgramManager programManagerService = tool.getService(ProgramManager.class); + Program[] allOpenPrograms = programManagerService.getAllOpenPrograms(); + assertEquals(0, allOpenPrograms.length); + + values.defineProgram(NAME, this, tool); + showDialogOnSwingWithoutBlocking(); + setProjectFileOnProjectTree(values.getAbstractValue(NAME), programA.getDomainFile()); + pressOk(); + + allOpenPrograms = programManagerService.getAllOpenPrograms(); + assertEquals(1, allOpenPrograms.length); + assertEquals(programA, allOpenPrograms[0]); + } + +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFileValueTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFileValueTest.java new file mode 100644 index 0000000000..9dd86d5a48 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFileValueTest.java @@ -0,0 +1,135 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.values.ProjectFileValue; + +public class ProjectFileValueTest extends AbstractValueIntegrationTest { + private static final String NAME = "Project File"; + + @Test + public void testProjectFileValueNoDefault() { + values.defineProjectFile(NAME, null); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + + values.setProjectFile(NAME, fileA); + assertTrue(values.hasValue(NAME)); + + assertEquals(fileA, values.getProjectFile(NAME)); + } + + @Test + public void testProjectFileValueWithDefault() { + values.defineProjectFile(NAME, fileA); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(fileA, values.getProjectFile(NAME)); + + values.setProjectFile(NAME, fileB); + assertTrue(values.hasValue(NAME)); + + assertEquals(fileB, values.getProjectFile(NAME)); + + values.setProjectFile(NAME, null); + assertFalse(values.hasValue(NAME)); + } + + @Test + public void testGetAsText() { + ProjectFileValue value1 = new ProjectFileValue(NAME); + ProjectFileValue value2 = new ProjectFileValue(NAME, fileA); + assertNull(value1.getAsText()); + assertEquals("/A/A", value2.getAsText()); + } + + @Test + public void testSetAsText() { + ProjectFileValue v = new ProjectFileValue(NAME); + assertEquals(fileA, v.setAsText("/A/A")); + try { + v.setAsText(null); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + try { + v.setAsText("/zasd/asdfas"); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineProjectFile(NAME, null); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertFalse(values.hasValue(NAME)); + assertNull(values.getProjectFile(NAME)); + } + + @Test + public void testNoDefaultValueWithDialogInput() { + values.defineProjectFile(NAME, null); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setProjectFileOnProjectTree(values.getAbstractValue(NAME), fileA); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(fileA, values.getProjectFile(NAME)); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineProjectFile(NAME, fileA); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(fileA, values.getProjectFile(NAME)); + } + + @Test + public void testDefaultValueWithDialogInput() { + values.defineProjectFile(NAME, fileA); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setProjectFileOnProjectTree(values.getAbstractValue(NAME), fileB); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(fileB, values.getProjectFile(NAME)); + } + +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFolderValueTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFolderValueTest.java new file mode 100644 index 0000000000..6104094f95 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFolderValueTest.java @@ -0,0 +1,137 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.values.ProjectFolderValue; + +public class ProjectFolderValueTest extends AbstractValueIntegrationTest { + private static final String NAME = "Project File"; + + @Test + public void testProjectFolderValueNoDefault() { + values.defineProjectFolder(NAME, null); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + + values.setProjectFolder(NAME, folder); + assertTrue(values.hasValue(NAME)); + + assertEquals(folder, values.getProjectFolder(NAME)); + } + + @Test + public void testProjectFolderValueWithDefault() { + values.defineProjectFolder(NAME, rootFolder); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(rootFolder, values.getProjectFolder(NAME)); + + values.setProjectFolder(NAME, folder); + assertTrue(values.hasValue(NAME)); + + assertEquals(folder, values.getProjectFolder(NAME)); + + values.setProjectFolder(NAME, null); + assertFalse(values.hasValue(NAME)); + } + + @Test + public void testGetAsText() { + ProjectFolderValue value1 = new ProjectFolderValue(NAME); + ProjectFolderValue value2 = new ProjectFolderValue(NAME, folder); + assertNull(value1.getAsText()); + assertEquals("/A", value2.getAsText()); + } + + @Test + public void testSetAsText() { + ProjectFolderValue v = new ProjectFolderValue(NAME); + assertEquals(folder, v.setAsText("/A")); + try { + v.setAsText(null); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + try { + v.setAsText("/zasd/asdfas"); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineProjectFolder(NAME); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + // usually, this would have no value, but the root folder is such an obvious value + // in nothing is entered, we use that. + assertTrue(values.hasValue(NAME)); + assertEquals(rootFolder, values.getProjectFolder(NAME)); + } + + @Test + public void testNoDefaultValueWithDialogInput() { + values.defineProjectFolder(NAME); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setProjectFolderOnProjectTree(values.getAbstractValue(NAME), folder); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(folder, values.getProjectFolder(NAME)); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineProjectFolder(NAME, folder); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(folder, values.getProjectFolder(NAME)); + } + + @Test + public void testDefaultValueWithDialogInput() { + values.defineProjectFolder(NAME, rootFolder); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setProjectFolderOnProjectTree(values.getAbstractValue(NAME), folder); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(folder, values.getProjectFolder(NAME)); + } + +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ValuesMapDialogParseErrorTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ValuesMapDialogParseErrorTest.java new file mode 100644 index 0000000000..34325b409b --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ValuesMapDialogParseErrorTest.java @@ -0,0 +1,49 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import docking.widgets.values.AbstractValue; +import ghidra.app.util.AddressInput; + +public class ValuesMapDialogParseErrorTest extends AbstractValueIntegrationTest { + + @Test + public void testParseErrorBlocksDialogFromClosing() { + values.defineString("Name"); + values.defineAddress("Start", programA); + values.defineInt("Size"); + showDialogOnSwingWithoutBlocking(); + setTextOnAddressInput(values.getAbstractValue("Start"), "sdfasf"); + pressOk(); + + assertTrue(dialog.isShowing()); + assertTrue(dialog.getStatusText().startsWith("Error")); + setTextOnAddressInput(values.getAbstractValue("Start"), "0"); + pressOk(); + assertFalse(dialog.isShowing()); + } + + protected void setTextOnAddressInput(AbstractValue nameValue, String text) { + runSwing(() -> { + AddressInput addressInput = (AddressInput) nameValue.getComponent(); + addressInput.setValue(text); + }); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/ScrollableTextArea.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/ScrollableTextArea.java index 221667a736..6b9798f96a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/ScrollableTextArea.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/ScrollableTextArea.java @@ -222,7 +222,7 @@ public class ScrollableTextArea extends JScrollPane { * used by all constructors to finish initialization of the object */ private void initialize() { - textArea.setLineWrap(false); + textArea.setLineWrap(true); this.setAutoscrolls(true); this.setViewportView(textArea); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/AbstractValue.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/AbstractValue.java new file mode 100644 index 0000000000..34fce45f78 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/AbstractValue.java @@ -0,0 +1,150 @@ +/* ### + * 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.values; + +import java.util.Objects; + +import javax.swing.JComponent; + +/** + * Abstract base class for defined name/values in a {@link GValuesMap} and whose values can be + * edited in the {@link ValuesMapDialog}. Its main purpose is to provide a JComponent for + * editing the value. Generally, objects of this type can be in one of two states: having a value + * or not. This can be useful for validating the dialog input values to ensure the user enters + * a value. + *

+ * There are two situations where parse/conversion exceptions can occur in subclass implementations. + * One is the {@link #setAsText(String)} method. The subclass should catch any specific expected + * exception when parsing the string and convert it to an IllegalArgumentException. The other method + * is the {@link #updateValueFromComponent()} method which may also need to parse string data. In + * this case any expected exception should be converted to {@link ValuesMapParseException}. This + * is the only exception type the dialog will be trapping and displaying error messages for in the + * {@link ValuesMapDialog}. Any other type of exception will be considered unexpected and a + * programing error and will be eventally be handled by the default application error handler. + * + * @param The type of the value stored and edited by this class + */ +public abstract class AbstractValue { + private final String name; + private T value; + + /** + * Constructor that assigned a name and optional initial value for this object. + * @param name the name associated with this value. + * @param defaultValue an optional initial value for this object + */ + protected AbstractValue(String name, T defaultValue) { + this.name = Objects.requireNonNull(name); + this.value = defaultValue; + } + + /** + * Returns the name of this value object. + * @return the name of this value object + */ + public String getName() { + return name; + } + + /** + * Returns the value currently assigned to this object. + * @return the value currently assigned to this object (may be null) + */ + public T getValue() { + return value; + } + + /** + * Sets the value for this object. + * @param value the value to set for this object (may be null) + */ + public void setValue(T value) { + this.value = value; + } + + /** + * Copies the T value from the given AbstractValue to this AbstractValue. + * @param other the AbstractValue to copy from + */ + public void copyValue(AbstractValue other) { + setValue(other.getValue()); + } + + /** + * Returns true if the value is non-null. + * @return true if the value is non-null + */ + public boolean hasValue() { + return value != null; + } + + /** + * Sets the value for this object from the given string. If this object can not succesfully + * parse the string, an exception will be thrown. + * @param valueString the string to be parsed into the type for this object + * @return The value resulting from parsing the string value + * @throws IllegalArgumentException if the string can not be parsed into a value of type T + */ + public T setAsText(String valueString) { + if (valueString == null) { + throw new IllegalArgumentException("Value string can not be null!"); + } + value = fromString(valueString); + return value; + } + + /** + * Returns a string representation for the value. It is expected that the string returned + * from this method can be parsed by the corresponding {@link #setAsText(String)} method. If the + * value of this object is null, null will be returned. + * @return a string representation for the value or null if the value is null + */ + public String getAsText() { + return value == null ? null : toString(value); + } + + protected String toString(T t) { + return t.toString(); + } + + /** + * Returns a JComponent for entering or editing a value of this type. + * @return a JComponent for entering or editing a value of this type. + */ + public abstract JComponent getComponent(); + + /** + * Causes the stored value for this object to be updated based on the state of the + * JComponent returned from {@link #getComponent()} + * @throws ValuesMapParseException if an error occurs trying update the value from a + * component. This usually is a result of trying to parse a string value. + */ + protected abstract void updateValueFromComponent() throws ValuesMapParseException; + + /** + * Updates the JComponent returned from {@link #getComponent()} to represent the current + * value of this object. + */ + protected abstract void updateComponentFromValue(); + + /** + * Parses the given string into a value of type T + * @param valueString the string to parse + * @return a value of type T + */ + protected abstract T fromString(String valueString); + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/BooleanValue.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/BooleanValue.java new file mode 100644 index 0000000000..b661a5712b --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/BooleanValue.java @@ -0,0 +1,63 @@ +/* ### + * 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.values; + +import javax.swing.JCheckBox; +import javax.swing.JComponent; + +/** + * Value class for {@link Boolean} types. Boolean types use a {@link JCheckBox} for displaying and + * modifying values. Because the checkBox is always either checked or unchecked, + * BooleanValues don't support the concept of having no value. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. */ +public class BooleanValue extends AbstractValue { + + private JCheckBox checkBox; + + BooleanValue(String name, boolean defaultValue) { + super(name, defaultValue); + } + + @Override + public JComponent getComponent() { + if (checkBox == null) { + checkBox = new JCheckBox(); + } + return checkBox; + } + + @Override + protected void updateValueFromComponent() { + setValue(checkBox.isSelected()); + } + + @Override + protected void updateComponentFromValue() { + checkBox.setSelected(getValue()); + } + + @Override + protected Boolean fromString(String valueString) { + return Boolean.parseBoolean(valueString); + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ChoiceValue.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ChoiceValue.java new file mode 100644 index 0000000000..50b403f4d7 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ChoiceValue.java @@ -0,0 +1,82 @@ +/* ### + * 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.values; + +import javax.swing.JComponent; + +import docking.widgets.combobox.GComboBox; + +/** + * Value class for selecting from a restricted set of {@link String}s. ChoiceValues uses a + * {@link GComboBox} for the editor component. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. */ +public class ChoiceValue extends AbstractValue { + + private String[] choices; + private GComboBox combo; + + ChoiceValue(String name, String defaultValue, String... choices) { + super(name, defaultValue); + this.choices = choices; + if (defaultValue != null && !isValidChoice(defaultValue)) { + throw new IllegalArgumentException("Default value is not one of the valid choices!"); + } + } + + @Override + public JComponent getComponent() { + if (combo == null) { + combo = new GComboBox(choices); + } + return combo; + } + + @Override + protected void updateValueFromComponent() { + setValue((String) combo.getSelectedItem()); + } + + @Override + protected void updateComponentFromValue() { + combo.setSelectedItem(getValue()); + } + + @Override + protected String fromString(String valueString) { + if (valueString == null) { + return null; + } + if (isValidChoice(valueString)) { + return valueString; + } + throw new IllegalArgumentException(valueString + " is not a valid choice!"); + } + + private boolean isValidChoice(String valueString) { + for (String choice : choices) { + if (choice.equals(valueString)) { + return true; + } + } + return false; + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/DoubleValue.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/DoubleValue.java new file mode 100644 index 0000000000..813b5a466b --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/DoubleValue.java @@ -0,0 +1,79 @@ +/* ### + * 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.values; + +import javax.swing.JComponent; + +import docking.widgets.textfield.FloatingPointTextField; + +/** + * Value class for {@link Double} types. This value uses a {@link FloatingPointTextField} as it's + * editor component. It supports the concept of no value, if the text field is empty. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class DoubleValue extends AbstractValue { + private FloatingPointTextField field; + + public DoubleValue(String name) { + this(name, null); + } + + public DoubleValue(String name, Double defaultValue) { + super(name, defaultValue); + } + + @Override + public JComponent getComponent() { + if (field == null) { + field = new FloatingPointTextField(20); + } + return field; + } + + @Override + protected void updateValueFromComponent() { + String text = field.getText(); + + // special case where user didn't enter a value on a string field that was defined without + // a value + if (getValue() == null && text.equals("")) { + return; + } + setValue(field.getValue()); + } + + @Override + protected void updateComponentFromValue() { + Double value = getValue(); + if (value == null) { + field.setText(""); + return; + } + field.setValue(value); + } + + @Override + public Double fromString(String valueString) { + return Double.parseDouble(valueString); + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/FileValue.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/FileValue.java new file mode 100644 index 0000000000..6c5761a54f --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/FileValue.java @@ -0,0 +1,146 @@ +/* ### + * 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.values; + +import java.awt.BorderLayout; +import java.io.File; + +import javax.swing.*; + +import docking.widgets.button.BrowseButton; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; + +/** + * Value class for {@link File} types. FileValues can be used for either file or directory values, + * depending on the constructor options. The editor component uses a {@link JTextField} with + * a browse button for bringing up a {@link GhidraFileChooser} for picking files or directories. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class FileValue extends AbstractValue { + private GhidraFileChooserMode chooserMode; + private File startingDir; + private FileValuePanel filePanel; + + /** + * Constructs a FileValue that expects its value to represent a file and not a directory. + * @param name the name of the value + */ + public FileValue(String name) { + this(name, null); + } + + /** + * Constructs a FileValue that expects its value to represent a file and not a directory. + * @param name the name of the value + * @param defaultValue the optional default File value. + */ + public FileValue(String name, File defaultValue) { + this(name, defaultValue, null, GhidraFileChooserMode.FILES_AND_DIRECTORIES); + } + + /** + * Constructs a FileValue that could represent either a File or Directory, depending on the + * mode value. + * @param name the name of the value + * @param defaultValue the optional default File value. If non-null this can be either a + * file or directory, but it should match the given {@link GhidraFileChooserMode} + * @param startingDir an optional directory specifying where the FileChooser should intialize + * its starting selected directory. + * @param mode the {@link GhidraFileChooserMode} used to indicate if this File represents a + * file or directory. It will put the GhidraFileChooser in a mode for choosing files or + * directories. + */ + public FileValue(String name, File defaultValue, File startingDir, GhidraFileChooserMode mode) { + super(name, defaultValue); + this.chooserMode = mode; + this.startingDir = startingDir; + } + + @Override + public JComponent getComponent() { + if (filePanel == null) { + filePanel = new FileValuePanel(getName()); + } + return filePanel; + } + + @Override + public void updateValueFromComponent() { + setValue(filePanel.getFile()); + } + + @Override + public void updateComponentFromValue() { + filePanel.setValue(getValue()); + } + + @Override + public File fromString(String valueString) { + return new File(valueString); + } + + // not private so that tests can access this class + class FileValuePanel extends JPanel { + private JTextField textField; + private JButton browseButton; + + public FileValuePanel(String name) { + super(new BorderLayout()); + setName(name); + textField = new JTextField(20); + browseButton = new BrowseButton(); + browseButton.addActionListener(e -> showFileChooser()); + add(textField, BorderLayout.CENTER); + add(browseButton, BorderLayout.EAST); + } + + public void setValue(File value) { + String text = value == null ? "" : value.toString(); + textField.setText(text); + } + + private void showFileChooser() { + GhidraFileChooser chooser = new GhidraFileChooser(null); + chooser.setSelectedFile(getFile()); + chooser.setTitle("Choose " + getName()); + chooser.setFileSelectionMode(chooserMode); + if (startingDir != null) { + chooser.setCurrentDirectory(startingDir); + } + File selectedFile = chooser.getSelectedFile(); + if (selectedFile != null) { + textField.setText(selectedFile.toString()); + } + chooser.dispose(); + } + + public File getFile() { + String text = textField.getText().trim(); + if (text.isBlank()) { + return null; + } + return new File(text); + } + + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/GValuesMap.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/GValuesMap.java new file mode 100644 index 0000000000..d848a8953b --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/GValuesMap.java @@ -0,0 +1,556 @@ +/* ### + * 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.values; + +import java.io.File; +import java.util.*; + +import docking.widgets.filechooser.GhidraFileChooserMode; +import ghidra.util.StatusListener; + +/** + * Class for defining, storing, and retrieving groups of values of various types. The intended use + * is to create a ValuesMap, define some named values, and then invoke the ValuesMapDialog to allow + * the user to fill in values for the defined values. It also has a rich set of convenience methods + * for adding predefined value types to the map. Users can also directly add custom value types by + * using the {@link #addValue(AbstractValue)} method. + */ +public class GValuesMap { + + protected Map> valuesMap = new LinkedHashMap<>(); + private ValuesMapValidator validator; + + /** + * Returns a collection of the AbstractValues defined in this ValuesMap. + * @return a collection of the AbstractValues defined in this ValuesMap. + */ + public Collection> getValues() { + return valuesMap.values(); + } + + /** + * Adds an AbstractValue to this ValuesMap. This is a way to add a custom AbstractValue that + * doesn't have a convenience method for a predefine value type. + * @param value the AbstractValue to add to this ValuesMap + * @return returns the added value + */ + public AbstractValue addValue(AbstractValue value) { + String name = value.getName(); + checkDup(name); + valuesMap.put(name, value); + return value; + } + + /** + * Sets a {@link ValuesMapValidator}. If set, this will be called when the user presses the + * "Ok" button on the {@link ValuesMapDialog}. If the validator passes (returns true), then + * the dialog will close and return the user values. Otherwise, the dialog will display the + * error message (via the {@link StatusListener} in the + * {@link ValuesMapValidator#validate(GValuesMap, StatusListener)} call) and remain open. + * @param validator the validator to be called before returning from the dialog + */ + public void setValidator(ValuesMapValidator validator) { + this.validator = validator; + } + + /** + * The call to validate the data using the {@link ValuesMapValidator} set in the + * {@link #setValidator(ValuesMapValidator)} method. If no validator has been set, + * this method will return true. + * @param listener The {@link StatusListener} for reporting an error message. + * @return true if the validator passes or no validator has been set. + */ + public boolean isValid(StatusListener listener) { + if (validator != null) { + return validator.validate(this, listener); + } + return true; + } + + /** + * Updates each value in this ValuesMap from its corresponding JComponent. + * @throws ValuesMapParseException if any value encountered an error trying to update its + * value from the editor component. + */ + public void updateFromComponents() throws ValuesMapParseException { + for (AbstractValue inputValue : valuesMap.values()) { + inputValue.updateValueFromComponent(); + } + } + + /** + * Returns the AbstractValue for the given value name. + * @param name the name for which to get the AbstractValue + * @return the AbstractValue for the given value name. + */ + public AbstractValue getAbstractValue(String name) { + return valuesMap.get(name); + } + + /** + * Returns true if there is a defined value for the given name. + * @param name the name of the value to check for + * @return true if there is a defined value for the given name. + */ + public boolean isDefined(String name) { + return valuesMap.containsKey(name); + } + + /** + * Returns true if the value defined for the given name has a non-null value. + * @param name the name of the value + * @return true if the value defined for the given name has a non-null value. + */ + public boolean hasValue(String name) { + AbstractValue abstractValue = valuesMap.get(name); + if (abstractValue == null) { + throw new IllegalArgumentException("No value defined for " + name); + } + return abstractValue.hasValue(); + } + + /** + * Copies the values (not the AbstractValues objects, but the T values of each AbstractValue) + * from the given map into this map. The given map must have exactly the same name and + * AbstractValue types as this map. + * @param otherMap The GValuesMap to copy values from + * @throws IllegalArgumentException if the given map does not have exactly the same set of + * names and types as this this map + */ + @SuppressWarnings("unchecked") + public void copyValues(GValuesMap otherMap) { + for (AbstractValue v : valuesMap.values()) { + AbstractValue otherValue = otherMap.getAbstractValue(v.getName()); + if (otherValue == null || otherValue.getClass() != v.getClass()) { + throw new IllegalArgumentException( + "Can't copy values from incompatable " + getClass().getSimpleName() + "s!"); + } + v.copyValue(v.getClass().cast(otherValue)); + } + } + +//================================================================================================== +// Define Value Methods +//================================================================================================== + + /** + * Defines a value of type Boolean. + * @param name the name for this value + * @param defaultValue the default value for this boolean value. + * @return the new BooleanValue that was defined. + */ + public BooleanValue defineBoolean(String name, boolean defaultValue) { + checkDup(name); + BooleanValue value = new BooleanValue(name, defaultValue); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type String, but with a restricted set of valid string values. + * @param name the name for this value. + * @param defaultValue an optional (can be null) initial value + * @param choices varargs list of valid string choices + * @return the new ChoiceValue that was defined + */ + public ChoiceValue defineChoice(String name, String defaultValue, String... choices) { + checkDup(name); + ChoiceValue value = new ChoiceValue(name, defaultValue, choices); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type File, but is restricted to directories. + * @param name the name for this value + * @param defaultValue an optional initial value + * @return the new FileValue that was defined + */ + public FileValue defineDirectory(String name, File defaultValue) { + checkDup(name); + FileValue value = + new FileValue(name, defaultValue, null, GhidraFileChooserMode.DIRECTORIES_ONLY); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Double with no initial default value. + * @param name the name for this value + * @return the new DoubleValue that was defined + */ + public DoubleValue defineDouble(String name) { + checkDup(name); + DoubleValue value = new DoubleValue(name, null); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Double with an initial value + * @param name the name for this value + * @param defaultValue the initial value + * @return the new DoubleValue that was defined + */ + public DoubleValue defineDouble(String name, double defaultValue) { + checkDup(name); + DoubleValue value = new DoubleValue(name, defaultValue); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type File + * @param name the name for this value + * @param defaultValue an optional initial value + * @return the new FileValue that was defined + */ + public FileValue defineFile(String name, File defaultValue) { + return defineFile(name, defaultValue, null); + } + + /** + * Defines a value of type File + * @param name the name for this value + * @param defaultValue an optional initial value + * @param startingDir specifies the starting directory when the FileChooser is invoked + * @return the new FileValue that was defined + */ + public FileValue defineFile(String name, File defaultValue, File startingDir) { + checkDup(name); + FileValue value = + new FileValue(name, defaultValue, startingDir, GhidraFileChooserMode.FILES_ONLY); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Integer that displays as a hex value. + * @param name the name for this value + * @return the new IntValue that was defined + */ + public IntValue defineHexInt(String name) { + checkDup(name); + IntValue value = new IntValue(name, null, true); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Integer with an initial value and displays as a hex value. + * @param name the name for this value + * @param defaultValue the initial value + * @return the new IntValue that was defined + */ + public IntValue defineHexInt(String name, int defaultValue) { + checkDup(name); + IntValue value = new IntValue(name, defaultValue, true); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Long that displays as a hex value. + * @param name the name for this value + * @return the new LongValue that was defined + */ + public LongValue defineHexLong(String name) { + checkDup(name); + LongValue value = new LongValue(name, null, true); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Long with an initial value and displays as a hex value. + * @param name the name for this value + * @param defaultValue the initial value + * @return the new LongValue that was defined + */ + public LongValue defineHexLong(String name, long defaultValue) { + checkDup(name); + LongValue value = new LongValue(name, defaultValue, true); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Integer with no initial value. + * @param name the name for this value + * @return the new IntValue that was defined + */ + public IntValue defineInt(String name) { + checkDup(name); + IntValue value = new IntValue(name, null, false); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Integer with an initial value. + * @param name the name for this value + * @param defaultValue the initial value + * @return the new IntValue that was defined + */ + public IntValue defineInt(String name, int defaultValue) { + checkDup(name); + IntValue value = new IntValue(name, defaultValue, false); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Long with an initial value. + * @param name the name for this value + * @return the new LongValue that was defined + */ + public LongValue defineLong(String name) { + checkDup(name); + LongValue value = new LongValue(name, null, false); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type Long with an initial value. + * @param name the name for this value + * @param defaultValue the initial value + * @return the new LongValue that was defined + */ + public LongValue defineLong(String name, long defaultValue) { + checkDup(name); + LongValue value = new LongValue(name, defaultValue, false); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type String. + * @param name the name for this value + * @return the new StringValue that was defined + */ + public StringValue defineString(String name) { + checkDup(name); + StringValue value = new StringValue(name, null); + valuesMap.put(name, value); + return value; + } + + /** + * Defines a value of type String with an optional initial value + * @param name the name for this value + * @param defaultValue the initial value (can be null) + * @return the new StringValue that was defined + */ + public StringValue defineString(String name, String defaultValue) { + checkDup(name); + StringValue value = new StringValue(name, defaultValue); + valuesMap.put(name, value); + return value; + } + +//================================================================================================== +// Get Value Methods +//================================================================================================== + + /** + * Gets the boolean value for the given name. + * @param name the name of a previously defined boolean value + * @return the boolean value + * @throws IllegalArgumentException if the name hasn't been defined as a boolean type + */ + public boolean getBoolean(String name) { + BooleanValue booleanValue = getValue(name, BooleanValue.class, "Boolean"); + Boolean value = booleanValue.getValue(); + return value == null ? false : value; + } + + /** + * Gets the Choice (String) value for the given name. The value will be either null or one of + * the strings that were defined as valid choices. + * @param name the name of a previously defined Choice value + * @return the Choice value + * @throws IllegalArgumentException if the name hasn't been defined as a Choice type + */ + public String getChoice(String name) { + ChoiceValue choiceValue = getValue(name, ChoiceValue.class, "Choice"); + return choiceValue.getValue(); + } + + /** + * Gets the double value for the given name. + * @param name the name of a previously defined double value + * @return the double value + * @throws IllegalArgumentException if the name hasn't been defined as a double type + */ + public double getDouble(String name) { + DoubleValue doubleValue = getValue(name, DoubleValue.class, "Double"); + Double value = doubleValue.getValue(); + return value == null ? 0.0 : value; + } + + /** + * Gets the {@link File} value for the given name. + * @param name the name of a previously defined File value + * @return the File value + * @throws IllegalArgumentException if the name hasn't been defined as a File type + */ + public File getFile(String name) { + FileValue fileValue = getValue(name, FileValue.class, "File"); + return fileValue.getValue(); + } + + /** + * Gets the int value for the given name. + * @param name the name of a previously defined int value + * @return the int value + * @throws IllegalArgumentException if the name hasn't been defined as a int type + */ + public int getInt(String name) { + IntValue intValue = getValue(name, IntValue.class, "Int"); + Integer value = intValue.getValue(); + return value == null ? 0 : value; + + } + + /** + * Gets the long value for the given name. + * @param name the name of a previously defined long value + * @return the long value + * @throws IllegalArgumentException if the name hasn't been defined as a long type + */ + public long getLong(String name) { + LongValue longValue = getValue(name, LongValue.class, "Int"); + Long value = longValue.getValue(); + return value == null ? 0 : value; + } + + /** + * Gets the String value for the given name. + * @param name the name of a previously defined String value + * @return the String value + * @throws IllegalArgumentException if the name hasn't been defined as a String type + */ + public String getString(String name) { + StringValue stringValue = getValue(name, StringValue.class, "String"); + return stringValue.getValue(); + } + +//================================================================================================== +// Set Value Methods +//================================================================================================== + + /** + * Sets the boolean value for the given name. + * @param name the name of the boolean value that was previously defined + * @param value the boolean to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as a boolean type + */ + public void setBoolean(String name, boolean value) { + BooleanValue booleanValue = getValue(name, BooleanValue.class, "Boolean"); + booleanValue.setValue(value); + } + + /** + * Sets the Choice (String) value for the given name. + * @param name the name of the Choice value that was previously defined + * @param choice the string to set as the value. This String must be one of the defined choices + * @throws IllegalArgumentException if the name hasn't been defined as a choice type + */ + public void setChoice(String name, String choice) { + ChoiceValue choiceValue = getValue(name, ChoiceValue.class, "Choice"); + choiceValue.setValue(choice); + } + + /** + * Sets the double value for the given name. + * @param name the name of the double value that was previously defined + * @param value the double to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as a double type + */ + public void setDouble(String name, double value) { + DoubleValue doubleValue = getValue(name, DoubleValue.class, "Double"); + doubleValue.setValue(value); + } + + /** + * Sets the {@link File} value for the given name. + * @param name the name of the File value that was previously defined + * @param value the File to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as a File type + */ + public void setFile(String name, File value) { + FileValue fileValue = getValue(name, FileValue.class, "File"); + fileValue.setValue(value); + } + + /** + * Sets the int value for the given name. + * @param name the name of the int value that was previously defined + * @param value the int to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as a int type + */ + public void setInt(String name, int value) { + IntValue intValue = getValue(name, IntValue.class, "Int"); + intValue.setValue(value); + } + + /** + * Sets the long value for the given name. + * @param name the name of the long value that was previously defined + * @param value the long to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as a long type + */ + public void setLong(String name, long value) { + LongValue intValue = getValue(name, LongValue.class, "Long"); + intValue.setValue(value); + } + + /** + * Sets the String value for the given name. + * @param name the name of the String value that was previously defined + * @param value the String to set as the value + * @throws IllegalArgumentException if the name hasn't been defined as a String type + */ + public void setString(String name, String value) { + StringValue stringValue = getValue(name, StringValue.class, "String"); + stringValue.setValue(value); + } + +//================================================================================================== +// Protected Methods +//================================================================================================== + + @SuppressWarnings("unchecked") + protected T getValue(String name, Class c, String typeName) { + AbstractValue value = valuesMap.get(name); + if (value == null) { + throw new IllegalArgumentException("No value defined for " + name); + } + if (value.getClass().isAssignableFrom(c)) { + return (T) value; + } + throw new IllegalArgumentException( + "Wrong type! No " + typeName + " value defined for: " + name); + } + + protected void checkDup(String name) { + if (valuesMap.containsKey(name)) { + throw new IllegalArgumentException("value already exits named " + name); + } + } + +} 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 new file mode 100644 index 0000000000..57d89707cf --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/IntValue.java @@ -0,0 +1,114 @@ +/* ### + * 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.values; + +import javax.swing.JComponent; + +import docking.widgets.textfield.IntegerTextField; + +/** + * Value class for {@link Integer} Value with an option for display the value as decimal or hex. The + * editor component uses an {@link IntegerTextField} for display and editing the value. This + * value supports the concept of no value which is represented by the text field being empty. If + * the text field is not empty, then the field only allows valid numeric values. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class IntValue extends AbstractValue { + private boolean displayAsHex; + private IntegerTextField field; + + /** + * Constructs an IntValue that displays it value in decimal + * @param name the name of the value + */ + public IntValue(String name) { + this(name, null, false); + } + + /** + * Constructs an IntValue with a default value that displays it value in decimal + * @param name the name of the value + * @param defaultValue the default value + */ + public IntValue(String name, int defaultValue) { + this(name, defaultValue, false); + } + + /** + * Constructs an IntValue with a default value. + * @param name the name of the value + * @param defaultValue the default value + * @param displayAsHex if true, the value will be displayed as hex, otherwise it will display + * as decimal. + */ + public IntValue(String name, Integer defaultValue, boolean displayAsHex) { + super(name, defaultValue); + this.displayAsHex = displayAsHex; + } + + @Override + public JComponent getComponent() { + if (field == null) { + field = new IntegerTextField(20); + field.setAllowsHexPrefix(false); + field.setShowNumberMode(false); + if (displayAsHex) { + field.setHexMode(); + field.setShowNumberMode(true); + } + } + return field.getComponent(); + } + + @Override + protected void updateValueFromComponent() { + String text = field.getText(); + + // special case where user didn't enter a value on a string field that was defined without + // a value + if (getValue() == null && text.equals("")) { + return; + } + setValue(field.getIntValue()); + } + + @Override + protected void updateComponentFromValue() { + Integer value = getValue(); + if (value == null) { + field.setText(""); + return; + } + field.setValue(value); + } + + @Override + public Integer fromString(String valueString) { + return displayAsHex ? Integer.parseInt(valueString, 16) : Integer.parseInt(valueString, 10); + } + + @Override + public String toString(Integer v) { + return displayAsHex ? Integer.toHexString(v) : v.toString(); + } + +} 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 new file mode 100644 index 0000000000..be6680bfe1 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/LongValue.java @@ -0,0 +1,105 @@ +/* ### + * 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.values; + +import javax.swing.JComponent; + +import docking.widgets.textfield.IntegerTextField; + +/** + * Value class for Long Values with an option for display the value as decimal or hex. The + * editor component uses an {@link IntegerTextField} for display and editing the value. This + * value supports the concept of no value which is represented by the text field being empty. If + * the text field is not empty, then the field only allows valid numeric values. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class LongValue extends AbstractValue { + private boolean displayAsHex; + private IntegerTextField field; + + public LongValue(String name) { + this(name, null, false); + } + + public LongValue(String name, Long defaultValue) { + this(name, defaultValue, false); + } + + public LongValue(String name, boolean displayAsHex) { + this(name, null, displayAsHex); + } + + public LongValue(String name, Long defaultValue, boolean displayAsHex) { + super(name, defaultValue); + this.displayAsHex = displayAsHex; + } + + @Override + public JComponent getComponent() { + if (field == null) { + field = new IntegerTextField(20); + field.setAllowsHexPrefix(false); + field.setShowNumberMode(false); + if (displayAsHex) { + field.setHexMode(); + field.setShowNumberMode(true); + } + } + return field.getComponent(); + } + + @Override + protected void updateValueFromComponent() { + String text = field.getText(); + + // special case where user didn't enter a value on a string field that was defined without + // a value + if (getValue() == null && text.equals("")) { + return; + } + setValue(field.getLongValue()); + } + + @Override + protected void updateComponentFromValue() { + Long value = getValue(); + if (value == null) { + field.setText(""); + return; + } + field.setValue(value); + } + + @Override + public Long fromString(String valueString) { + return displayAsHex ? Long.parseLong(valueString, 16) : Long.parseLong(valueString, 10); + } + + @Override + public String getAsText() { + Long v = getValue(); + if (v == null) { + return null; + } + return displayAsHex ? Long.toHexString(v) : Long.toString(v); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/StringValue.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/StringValue.java new file mode 100644 index 0000000000..8323c836a5 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/StringValue.java @@ -0,0 +1,72 @@ +/* ### + * 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.values; + +import javax.swing.JComponent; +import javax.swing.JTextField; + +/** + * Value class for {@link String} values. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class StringValue extends AbstractValue { + private JTextField textField; + + public StringValue(String name) { + this(name, null); + } + + public StringValue(String name, String defaultValue) { + super(name, defaultValue); + } + + @Override + public JComponent getComponent() { + if (textField == null) { + textField = new JTextField(20); + } + return textField; + } + + @Override + protected void updateValueFromComponent() { + String text = textField.getText(); + + // special case where user didn't enter a value on a string field that was defined without + // a value + if (getValue() == null && text.equals("")) { + return; + } + setValue(text); + } + + @Override + protected void updateComponentFromValue() { + textField.setText(getValue()); + } + + @Override + public String fromString(String valueString) { + + return valueString; + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapDialog.java new file mode 100644 index 0000000000..049e7d6839 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapDialog.java @@ -0,0 +1,168 @@ +/* ### + * 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.values; + +import java.awt.BorderLayout; + +import javax.swing.*; + +import docking.DialogComponentProvider; +import docking.widgets.label.GHtmlLabel; +import ghidra.util.HTMLUtilities; +import ghidra.util.MessageType; +import ghidra.util.layout.PairLayout; + +/** + * Dialog for displaying and editing values defined in a {@link GValuesMap}. The dialog consists + * of an option message, followed by a list of name / value pairs. The name / value pairs will + * be display in the order they were defined in the ValuesMap. + */ +public class ValuesMapDialog extends DialogComponentProvider { + + private static final int MAX_MESSAGE_LINE_WIDTH = 60; + private JPanel valuesPanel; + private GValuesMap valuesMap; + private boolean cancelled = false; + private String message; + + /** + * Creates the dialog with the given title and optional message. The message will be display + * at the top of the dialog before the list of name / value pairs. This form of the dialog + * requires that the {@link #addValue(AbstractValue)} method be called to populate the + * ValuesMap. + * @param title the title for the dialog + * @param message the optional message to display before the list of name value pairs + */ + public ValuesMapDialog(String title, String message) { + this(title, message, new GValuesMap()); + } + + /** + * Creates the dialog with the given title and optional message. The message will be display + * at the top of the dialog before the list of name / value pairs. The values are provided + * at construction time. + * @param title the title for the dialog + * @param message the optional message to display before the list of name value pairs + * @param valuesMap the ValuesMap whose values are to be displayed. + */ + public ValuesMapDialog(String title, String message, GValuesMap valuesMap) { + super(title); + this.message = message; + this.valuesMap = valuesMap; + + valuesPanel = buildValuesPanel(); + + addWorkPanel(buildWorkPanel()); + + for (AbstractValue value : valuesMap.getValues()) { + buildComponentsForValue(value); + } + setRememberSize(false); + + addOKButton(); + addCancelButton(); + } + + /** + * Adds a new value to the ValuesMap being edited by this dialog. + * @param value the new AbstractValue to be added + * @return the value that was added + */ + public AbstractValue addValue(AbstractValue value) { + valuesMap.addValue(value); + buildComponentsForValue(value); + return value; + } + + /** + * Sets the {@link ValuesMapValidator} on the ValuesMap being edited. This is usually set on the + * ValuesMap before the dialog is constructed. This method is for uses where it wasn't + * constructed with a ValueMap, but values were added directly to the dialog after dialog + * construction. + * @param validator the ValuesMapValidator + */ + public void setValidator(ValuesMapValidator validator) { + valuesMap.setValidator(validator); + } + + /** + * Returns the ValuesMap being edited. + * @return the ValuesMap being edited. + */ + public GValuesMap getValues() { + if (cancelled) { + return null; + } + return valuesMap; + } + + /** + * Returns true if the dialog was cancelled. + * @return true if the dialog was cancelled. + */ + public boolean isCancelled() { + return cancelled; + } + + @Override + protected void okCallback() { + try { + valuesMap.updateFromComponents(); + } + catch (ValuesMapParseException e) { + setStatusText(e.getMessage(), MessageType.ERROR); + return; + } + if (valuesMap.isValid(this)) { + close(); + } + } + + @Override + protected void cancelCallback() { + cancelled = true; + super.cancelCallback(); + } + + private JPanel buildValuesPanel() { + JPanel panel = new JPanel(new PairLayout(4, 10)); + panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + return panel; + } + + private JComponent buildWorkPanel() { + JPanel panel = new JPanel(new BorderLayout()); + if (message != null) { + String literalHTML = HTMLUtilities.toLiteralHTML(message, MAX_MESSAGE_LINE_WIDTH); + GHtmlLabel label = new GHtmlLabel(literalHTML); + + label.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + panel.add(label, BorderLayout.NORTH); + } + + JScrollPane scroll = new JScrollPane(valuesPanel); + scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + panel.add(scroll, BorderLayout.CENTER); + return panel; + } + + private void buildComponentsForValue(AbstractValue value) { + valuesPanel.add(new JLabel(value.getName() + ":", SwingConstants.RIGHT)); + valuesPanel.add(value.getComponent()); + value.updateComponentFromValue(); + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapParseException.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapParseException.java new file mode 100644 index 0000000000..c4d3ca5bdd --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapParseException.java @@ -0,0 +1,33 @@ +/* ### + * 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.values; + +/** + * Exception thrown when processing/parsing ValuesMap values. Mostly exists so that the exception + * message is uniform throught the types. + */ +public class ValuesMapParseException extends Exception { + + /** + * Constructor + * @param valueName the name of the value that was being processed + * @param type the type name of the value that was being processed + * @param message the detail message of what went wrong + */ + public ValuesMapParseException(String valueName, String type, String message) { + super("Error processing " + type + " value \"" + valueName + "\"! " + message); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapValidator.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapValidator.java new file mode 100644 index 0000000000..4174988f96 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapValidator.java @@ -0,0 +1,36 @@ +/* ### + * 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.values; + +import ghidra.util.StatusListener; + +/** + * Interface for validating values in a {@link GValuesMap} + */ +public interface ValuesMapValidator { + + /** + * Validates one or more values in the given ValuesMap. This is used by the ValuesMapDialog + * to validate values when the user presses the "Ok" button. If it returns true, the dialog + * will close. Otherwise, the dialog will remain visible, displaying the error message that + * was reported to the given StatusListener. + * @param values the ValuesMap whose values are to be validated + * @param statusListener a {@link StatusListener} to report validation errors back to + * the dialog + * @return true if the values pass the validation check. + */ + public boolean validate(GValuesMap values, StatusListener statusListener); +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/AbstractValueTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/AbstractValueTest.java new file mode 100644 index 0000000000..0d01672d42 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/AbstractValueTest.java @@ -0,0 +1,57 @@ +/* ### + * 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.values; + +import javax.swing.JButton; +import javax.swing.JTextField; + +import docking.DialogComponentProvider; +import docking.DockingWindowManager; +import docking.test.AbstractDockingTest; + +public abstract class AbstractValueTest extends AbstractDockingTest { + + protected ValuesMapDialog dialog; + protected GValuesMap values = new GValuesMap(); + + protected void showDialogOnSwingWithoutBlocking() { + + runSwing(() -> { + dialog = new ValuesMapDialog("Test", null, values); + DockingWindowManager.showDialog(dialog); + }, false); + + waitForDialogComponent(DialogComponentProvider.class); + } + + protected void setTextOnComponent(AbstractValue nameValue, String text) { + runSwing(() -> { + JTextField field = (JTextField) nameValue.getComponent(); + field.setText(text); + }); + } + + protected void pressOk() { + JButton okButton = (JButton) getInstanceField("okButton", dialog); + runSwing(() -> okButton.doClick()); + } + + protected void pressCancel() { + JButton okButton = (JButton) getInstanceField("cancelButton", dialog); + runSwing(() -> okButton.doClick()); + } + +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/BooleanValueTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/BooleanValueTest.java new file mode 100644 index 0000000000..fd40edab8c --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/BooleanValueTest.java @@ -0,0 +1,90 @@ +/* ### + * 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.values; + +import static org.junit.Assert.*; + +import javax.swing.JCheckBox; + +import org.junit.Test; + +import docking.widgets.values.AbstractValue; +import docking.widgets.values.BooleanValue; + +public class BooleanValueTest extends AbstractValueTest { + private static final String NAME = "YesNo"; + + @Test + public void testBooleanValue() { + values.defineBoolean(NAME, false); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(false, values.getBoolean(NAME)); + + values.setBoolean(NAME, true); + assertTrue(values.hasValue(NAME)); + + assertEquals(true, values.getBoolean(NAME)); + } + + @Test + public void testGetAsText() { + BooleanValue value1 = new BooleanValue(NAME, true); + BooleanValue value2 = new BooleanValue(NAME, false); + + assertEquals("true", value1.getAsText()); + assertEquals("false", value2.getAsText()); + } + + @Test + public void testSetAsText() { + BooleanValue result = new BooleanValue(NAME, false); + + assertTrue(result.setAsText("true")); + assertFalse(result.setAsText("false")); + assertTrue(result.setAsText("TRUE")); + assertFalse(result.setAsText("asdas")); + } + + @Test + public void testValueWithNoDialogInput() { + values.defineBoolean(NAME, false); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertEquals(false, values.getBoolean(NAME)); + } + + @Test + public void testValueWithDialogInput() { + values.defineBoolean(NAME, false); + + showDialogOnSwingWithoutBlocking(); + setBoolean(values.getAbstractValue(NAME), true); + pressOk(); + + assertEquals(true, values.getBoolean(NAME)); + } + + private void setBoolean(AbstractValue boolValue, boolean b) { + runSwing(() -> { + JCheckBox checkBox = (JCheckBox) boolValue.getComponent(); + checkBox.setSelected(b); + }); + } +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/ChoiceValueTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/ChoiceValueTest.java new file mode 100644 index 0000000000..5ec2d01a5c --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/ChoiceValueTest.java @@ -0,0 +1,151 @@ +/* ### + * 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.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import docking.widgets.combobox.GComboBox; +import docking.widgets.values.AbstractValue; +import docking.widgets.values.ChoiceValue; + +public class ChoiceValueTest extends AbstractValueTest { + private static final String NAME = "Choice"; + + @Test + public void testChoiceValueNoDefault() { + values.defineChoice(NAME, null, "A", "B", "C"); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + + values.setChoice(NAME, "B"); + assertTrue(values.hasValue(NAME)); + + assertEquals("B", values.getChoice(NAME)); + } + + @Test + public void testChoiceValueWithDefault() { + values.defineChoice(NAME, "A", "A", "B", "C"); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals("A", values.getChoice(NAME)); + + values.setChoice(NAME, "C"); + assertTrue(values.hasValue(NAME)); + + assertEquals("C", values.getChoice(NAME)); + + values.setChoice(NAME, null); + assertFalse(values.hasValue(NAME)); + } + + @Test + public void testGetAsText() { + ChoiceValue v1 = new ChoiceValue(NAME, "A", "A", "B"); + ChoiceValue v2 = new ChoiceValue(NAME, null, "A", "B"); + + assertEquals("A", v1.getAsText()); + assertNull(v2.getAsText()); + } + + @Test + public void testSetAsText() { + ChoiceValue result = new ChoiceValue(NAME, null, "A", "B"); + + assertEquals("A", result.setAsText("A")); + + try { + result.setAsText("Z"); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testChoiceWithInValidDefault() { + try { + values.defineChoice(NAME, "Z", "A", "B", "C"); + fail("Was able to set bad default value in choice"); + } + catch (IllegalArgumentException e) { + // expected + } + + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineChoice(NAME, null, "A", "B", "C"); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertFalse(values.hasValue(NAME)); + assertNull(values.getChoice(NAME)); + } + + @Test + public void testNoDefaultValueWithDialogInput() { + values.defineChoice(NAME, null, "A", "B", "C"); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setCombo(values.getAbstractValue(NAME), "C"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals("C", values.getChoice(NAME)); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineChoice(NAME, "B", "A", "B", "C"); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals("B", values.getChoice(NAME)); + } + + @Test + public void testDefaultValueWithDialogInput() { + values.defineChoice(NAME, "C", "A", "B", "C"); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setCombo(values.getAbstractValue(NAME), "A"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals("A", values.getChoice(NAME)); + } + + private void setCombo(AbstractValue choiceValue, String choice) { + runSwing(() -> { + GComboBox combo = (GComboBox) choiceValue.getComponent(); + combo.setSelectedItem(choice); + }); + } +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/DoubleValueTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/DoubleValueTest.java new file mode 100644 index 0000000000..f5fcc84693 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/DoubleValueTest.java @@ -0,0 +1,121 @@ +/* ### + * 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.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import docking.widgets.values.DoubleValue; + +public class DoubleValueTest extends AbstractValueTest { + private static final String NAME = "Fraction"; + private static double DELTA = 0.0001; + + @Test + public void testDoubleValueNoDefault() { + values.defineDouble(NAME); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + assertEquals(0, values.getDouble(NAME), DELTA); // the getPrimitive returns 0 when value is null + + values.setDouble(NAME, 6); + assertTrue(values.hasValue(NAME)); + + assertEquals(6, values.getDouble(NAME), DELTA); + } + + @Test + public void testDoubleValueWithDefault() { + values.defineDouble(NAME, 3.2); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(3.2, values.getDouble(NAME), DELTA); + + values.setDouble(NAME, 6.5); + assertTrue(values.hasValue(NAME)); + + assertEquals(6.5, values.getDouble(NAME), DELTA); + } + + @Test + public void testGetAsText() { + DoubleValue v1 = new DoubleValue(NAME, 1.23); + DoubleValue v2 = new DoubleValue(NAME); + assertEquals("1.23", v1.getAsText()); + assertNull(v2.getAsText()); + } + + @Test + public void testSetAsText() { + DoubleValue v1 = new DoubleValue(NAME); + + assertEquals((Double) 1.23, v1.setAsText("1.23")); + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineDouble(NAME); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertFalse(values.hasValue(NAME)); + assertEquals(0, values.getDouble(NAME), DELTA); + } + + @Test + public void testNoDefaultValueWithDialogInput() { + values.defineDouble(NAME); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(values.getAbstractValue(NAME), "1.23"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(1.23, values.getDouble(NAME), DELTA); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineDouble(NAME, 1.2); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(1.2, values.getDouble(NAME), DELTA); + } + + @Test + public void testDefaultValueWithDialogInput() { + values.defineDouble(NAME, 1.2); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(values.getAbstractValue(NAME), "4.3"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(4.3, values.getDouble(NAME), DELTA); + } + +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/FileValueTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/FileValueTest.java new file mode 100644 index 0000000000..3ca0a8c612 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/FileValueTest.java @@ -0,0 +1,204 @@ +/* ### + * 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.values; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; + +import org.junit.Test; + +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.values.AbstractValue; +import docking.widgets.values.FileValue; +import docking.widgets.values.FileValue.FileValuePanel; + +public class FileValueTest extends AbstractValueTest { + private static final String NAME = "My File"; + + @Test + public void testFileValueNoDefault() { + values.defineFile(NAME, null); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + + values.setFile(NAME, new File(".")); + assertTrue(values.hasValue(NAME)); + + assertEquals(new File("."), values.getFile(NAME)); + } + + @Test + public void testFileValueWithDefault() { + values.defineFile(NAME, new File("/")); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(new File("/"), values.getFile(NAME)); + + values.setFile(NAME, new File(".")); + assertTrue(values.hasValue(NAME)); + + assertEquals(new File("."), values.getFile(NAME)); + + values.setFile(NAME, null); + assertFalse(values.hasValue(NAME)); + } + + @Test + public void testGetAsText() { + FileValue value1 = new FileValue(NAME); + FileValue value2 = new FileValue(NAME, new File("/")); + assertNull(value1.getAsText()); + assertEquals("/", value2.getAsText()); + } + + @Test + public void testSetAsText() { + FileValue v = new FileValue(NAME); + assertEquals(new File("/abc"), v.setAsText("/abc")); + try { + v.setAsText(null); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testDirectoryValueNoDefault() { + values.defineDirectory(NAME, null); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + + values.setFile(NAME, new File(".")); + assertTrue(values.hasValue(NAME)); + + assertEquals(new File("."), values.getFile(NAME)); + } + + @Test + public void testDirectoryValueWithDefault() { + values.defineDirectory(NAME, new File("/")); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(new File("/"), values.getFile(NAME)); + + values.setFile(NAME, new File(".")); + assertTrue(values.hasValue(NAME)); + + assertEquals(new File("."), values.getFile(NAME)); + + values.setFile(NAME, null); + assertFalse(values.hasValue(NAME)); + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineFile(NAME, null); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertFalse(values.hasValue(NAME)); + assertNull(values.getFile(NAME)); + } + + @Test + public void testNoDefaultValueWithDialogInput() throws IOException { + File foo = createTempFile("foo"); + values.defineFile(NAME, null); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setFile(values.getAbstractValue(NAME), foo); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(foo, values.getFile(NAME)); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineFile(NAME, new File("/")); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(new File("/"), values.getFile(NAME)); + } + + @Test + public void testDefaultValueWithDialogInput() throws IOException { + File foo = createTempFile("foo"); + File bar = createTempFile("bar"); + values.defineFile(NAME, foo); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setFile(values.getAbstractValue(NAME), bar); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(bar, values.getFile(NAME)); + } + + @Test + public void testDirectoryWithDialogInput() throws IOException { + File dir = createTempDirectory("foo"); + values.defineDirectory(NAME, null); + showDialogOnSwingWithoutBlocking(); + setFile(values.getAbstractValue(NAME), dir); + pressOk(); + + assertEquals(dir, values.getFile(NAME)); + } + + @Test + public void testStartingDir() throws IOException { + File file = createTempDirectory("foo"); + File parent = file.getParentFile(); + values.defineFile(NAME, null, parent); + showDialogOnSwingWithoutBlocking(); + FileValuePanel fileWidget = (FileValuePanel) values.getAbstractValue(NAME).getComponent(); + pressButtonByName(fileWidget, "BrowseButton", false); + GhidraFileChooser chooser = waitForDialogComponent(GhidraFileChooser.class); + File dir = runSwing(() -> chooser.getCurrentDirectory()); + pressButtonByText(chooser, "Cancel"); + pressOk(); + + assertEquals(parent, dir); + } + + protected void setFile(AbstractValue nameValue, File f) { + FileValuePanel fileWidget = (FileValuePanel) nameValue.getComponent(); + pressButtonByName(fileWidget, "BrowseButton", false); + GhidraFileChooser chooser = waitForDialogComponent(GhidraFileChooser.class); + runSwing(() -> { + chooser.setSelectedFile(f); + }); + + pressButtonByText(chooser, "OK"); + } +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/IntValueTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/IntValueTest.java new file mode 100644 index 0000000000..5a4f9d9bf2 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/IntValueTest.java @@ -0,0 +1,137 @@ +/* ### + * 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.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import docking.widgets.values.IntValue; + +public class IntValueTest extends AbstractValueTest { + private static final String NAME = "Count"; + + @Test + public void testIntValueNoDefault() { + values.defineInt(NAME); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + assertEquals(0, values.getInt(NAME)); // the getPrimitive returns 0 when value is null + + values.setInt(NAME, 6); + assertTrue(values.hasValue(NAME)); + + assertEquals(6, values.getInt(NAME)); + } + + @Test + public void testIntValueWithDefault() { + values.defineInt(NAME, 32); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(32, values.getInt(NAME)); + + values.setInt(NAME, 6); + assertTrue(values.hasValue(NAME)); + + assertEquals(6, values.getInt(NAME)); + } + + @Test + public void testGetAsText() { + IntValue v1 = new IntValue(NAME, 12); + IntValue v2 = new IntValue(NAME); + IntValue v3 = new IntValue(NAME, 10, true /*displayAsHex*/); + assertEquals("12", v1.getAsText()); + assertNull(v2.getAsText()); + assertEquals("a", v3.getAsText()); + } + + @Test + public void testSetAsText() { + IntValue v1 = new IntValue(NAME); + IntValue v2 = new IntValue(NAME, null, true /*displayAsText*/); + + assertEquals((Integer) 10, v1.setAsText("10")); + assertEquals((Integer) 16, v2.setAsText("10")); + assertEquals((Integer) 10, v2.setAsText("A")); + assertEquals((Integer) 10, v2.setAsText("a")); + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineInt(NAME); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertFalse(values.hasValue(NAME)); + assertEquals(0, values.getInt(NAME)); + } + + @Test + public void testNoDefaultValueWithDialogInput() { + values.defineInt(NAME); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(values.getAbstractValue(NAME), "123"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(123, values.getInt(NAME)); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineInt(NAME, 12); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(12, values.getInt(NAME)); + } + + @Test + public void testDefaultValueWithDialogInput() { + values.defineInt(NAME, 12); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(values.getAbstractValue(NAME), "43"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(43, values.getInt(NAME)); + } + + @Test + public void testHexMode() { + values.defineHexInt(NAME, 12); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(values.getAbstractValue(NAME), "A"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(10, values.getInt(NAME)); + } +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/LongValueTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/LongValueTest.java new file mode 100644 index 0000000000..3b59f75b87 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/LongValueTest.java @@ -0,0 +1,137 @@ +/* ### + * 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.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import docking.widgets.values.LongValue; + +public class LongValueTest extends AbstractValueTest { + private static final String NAME = "Count"; + + @Test + public void testlongValueNoDefault() { + values.defineLong(NAME); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + assertEquals(0, values.getLong(NAME)); // the getPrimitive returns 0 when value is null + + values.setLong(NAME, 6); + assertTrue(values.hasValue(NAME)); + + assertEquals(6, values.getLong(NAME)); + } + + @Test + public void testlongValueWithDefault() { + values.defineLong(NAME, 32); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + assertEquals(32, values.getLong(NAME)); + + values.setLong(NAME, 6); + assertTrue(values.hasValue(NAME)); + + assertEquals(6, values.getLong(NAME)); + } + + @Test + public void testGetAsText() { + LongValue v1 = new LongValue(NAME, 12L); + LongValue v2 = new LongValue(NAME); + LongValue v3 = new LongValue(NAME, 10L, true /*displayAsHex*/); + assertEquals("12", v1.getAsText()); + assertNull(v2.getAsText()); + assertEquals("a", v3.getAsText()); + } + + @Test + public void testSetAsText() { + LongValue v1 = new LongValue(NAME); + LongValue v2 = new LongValue(NAME, null, true /*displayAsText*/); + + assertEquals((Long) 10L, v1.setAsText("10")); + assertEquals((Long) 16L, v2.setAsText("10")); + assertEquals((Long) 10L, v2.setAsText("A")); + assertEquals((Long) 10L, v2.setAsText("a")); + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineLong(NAME); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertFalse(values.hasValue(NAME)); + assertEquals(0, values.getLong(NAME)); + } + + @Test + public void testNoDefaultValueWithDialogInput() { + values.defineLong(NAME); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(values.getAbstractValue(NAME), "123"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(123, values.getLong(NAME)); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineLong(NAME, 12); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(12, values.getLong(NAME)); + } + + @Test + public void testDefaultValueWithDialogInput() { + values.defineLong(NAME, 12); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(values.getAbstractValue(NAME), "43"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(43, values.getLong(NAME)); + } + + @Test + public void testHexMode() { + values.defineHexLong(NAME, 12); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(values.getAbstractValue(NAME), "A"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals(10, values.getLong(NAME)); + } +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/StringValueTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/StringValueTest.java new file mode 100644 index 0000000000..690738345a --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/StringValueTest.java @@ -0,0 +1,129 @@ +/* ### + * 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.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import docking.widgets.values.StringValue; + +public class StringValueTest extends AbstractValueTest { + private static final String NAME = "Name"; + + @Test + public void testStringValueNoDefault() { + values.defineString(NAME); + + assertTrue(values.isDefined(NAME)); + assertFalse(values.hasValue(NAME)); + + values.setString(NAME, "abc"); + assertTrue(values.hasValue(NAME)); + + assertEquals("abc", values.getString(NAME)); + } + + @Test + public void testStringValueWithDefault() { + values.defineString(NAME, "ABC"); + + assertTrue(values.isDefined(NAME)); + assertTrue(values.hasValue(NAME)); + + values.setString(NAME, "xyz"); + assertTrue(values.hasValue(NAME)); + + assertEquals("xyz", values.getString(NAME)); + + values.setString(NAME, null); + assertFalse(values.hasValue(NAME)); + } + + @Test + public void testGetAsText() { + StringValue v1 = new StringValue(NAME, "A"); + StringValue v2 = new StringValue(NAME); + + assertEquals("A", v1.getAsText()); + assertNull(v2.getAsText()); + } + + @Test + public void testSetAsText() { + StringValue result = new StringValue(NAME); + + assertEquals("A", result.setAsText("A")); + + try { + result.setAsText(null); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testNoDefaultValueWithNoDialogInput() { + values.defineString(NAME); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertFalse(values.hasValue(NAME)); + assertNull(values.getString(NAME)); + } + + @Test + public void testNoDefaultValueWithDialogInput() { + values.defineString(NAME); + assertFalse(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(values.getAbstractValue(NAME), "xyz"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals("xyz", values.getString(NAME)); + } + + @Test + public void testDefaultValueWithNoDialogInput() { + values.defineString(NAME, "abc"); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals("abc", values.getString(NAME)); + } + + @Test + public void testDefaultValueWithDialogInput() { + values.defineString(NAME, "abc"); + assertTrue(values.hasValue(NAME)); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(values.getAbstractValue(NAME), "xyz"); + pressOk(); + + assertTrue(values.hasValue(NAME)); + assertEquals("xyz", values.getString(NAME)); + } +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/ValuesMapDialogTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/ValuesMapDialogTest.java new file mode 100644 index 0000000000..d647993a80 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/values/ValuesMapDialogTest.java @@ -0,0 +1,53 @@ +/* ### + * 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.values; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class ValuesMapDialogTest extends AbstractValueTest { + + @Test + public void testGetValueAsWrongType() { + AbstractValue ageValue = values.defineLong("Age"); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(ageValue, "42"); + pressOk(); + + try { + assertEquals(42, values.getInt("Age")); + fail("Should not be able to retrieve a value with the wrong type!"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testCancelDoesntChangeValue() { + AbstractValue nameValue = values.defineString("Name", "abc"); + + showDialogOnSwingWithoutBlocking(); + setTextOnComponent(nameValue, "Joe"); + pressCancel(); + + assertTrue(dialog.isCancelled()); + assertEquals("abc", values.getString("Name")); + } + +}