Merge remote-tracking branch 'origin/GP-61-dragonmacher-clear-keybindings'

This commit is contained in:
ghidravore 2020-08-21 11:15:31 -04:00
commit 64da286d3d
15 changed files with 434 additions and 482 deletions

View file

@ -116,8 +116,8 @@ public abstract class AbstractOptions implements Options {
"Attempted to register an unsupported object: " + defaultValue.getClass());
}
registerOption(optionName, OptionType.getOptionType(defaultValue), defaultValue, help,
description);
OptionType type = OptionType.getOptionType(defaultValue);
registerOption(optionName, type, defaultValue, help, description);
}
@Override
@ -150,16 +150,41 @@ public abstract class AbstractOptions implements Options {
valueMap.put(optionName, option);
return;
}
else if (!currentOption.isRegistered()) {
Option option =
Option newOption = null;
if (currentOption.isRegistered()) {
// Registered again
newOption =
copyRegisteredOption(currentOption, type, description, defaultValue, help, editor);
}
else {
// option was accessed, but not registered
newOption =
createRegisteredOption(optionName, type, description, help, defaultValue, editor);
option.setCurrentValue(currentOption.getCurrentValue());
valueMap.put(optionName, option);
return;
}
// TODO: We probably don't need to do anything special if we are re-registering an
// option, which is what the below code handles.
copyCurrentValue(currentOption, newOption);
valueMap.put(optionName, newOption);
}
private void copyCurrentValue(Option currentOption, Option newOption) {
Object currentValue = currentOption.getCurrentValue();
OptionType type = currentOption.getOptionType();
if (!isNullable(type) && currentValue == null) {
return; // not allowed to be null
}
// null is allowed; null can represent a valid 'cleared' state
newOption.setCurrentValue(currentValue);
}
private Option copyRegisteredOption(Option currentOption, OptionType type,
String description, Object defaultValue, HelpLocation help, PropertyEditor editor) {
// We probably don't need to do anything special if we are re-registering an option,
// which is what the below code handles
String oldDescription = currentOption.getDescription();
HelpLocation oldHelp = currentOption.getHelpLocation();
Object oldDefaultValue = currentOption.getDefaultValue();
@ -170,13 +195,9 @@ public abstract class AbstractOptions implements Options {
Object newDefaultValue = oldDefaultValue == null ? defaultValue : oldDefaultValue;
PropertyEditor newEditor = oldEditor == null ? editor : oldEditor;
Option newOption = createRegisteredOption(optionName, type, newDescripiton, newHelpLocation,
String optionName = currentOption.getName();
return createRegisteredOption(optionName, type, newDescripiton, newHelpLocation,
newDefaultValue, newEditor);
Object currentValue = currentOption.getCurrentValue();
if (currentValue != null) {
newOption.setCurrentValue(currentValue);
}
valueMap.put(optionName, newOption);
}
@Override
@ -249,6 +270,10 @@ public abstract class AbstractOptions implements Options {
}
OptionType type = option.getOptionType();
return isNullable(type);
}
private boolean isNullable(OptionType type) {
switch (type) {
// objects can be null
@ -396,8 +421,8 @@ public abstract class AbstractOptions implements Options {
public Font getFont(String optionName, Font defaultValue) {
Option option = getOption(optionName, OptionType.FONT_TYPE, defaultValue);
try {
return (Font) option.getValue(defaultValue);
}
return (Font) option.getValue(defaultValue);
}
catch (ClassCastException e) {
return defaultValue;
}

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,10 +15,12 @@
*/
package ghidra.framework.options;
import java.beans.PropertyEditor;
import java.util.Objects;
import ghidra.util.HelpLocation;
import ghidra.util.SystemUtilities;
import java.beans.PropertyEditor;
import utilities.util.reflection.ReflectionUtilities;
public abstract class Option {
private final String name;
@ -68,23 +69,19 @@ public abstract class Option {
return helpLocation;
}
public boolean hasValue() {
return defaultValue != null || getCurrentValue() != null;
}
public String getDescription() {
return description == null ? "Unregistered Option" : description;
}
public Object getValue(Object passedInDefaultValue) {
Object value = getCurrentValue();
if (value != null) {
return value;
if (value == null && defaultValue == null) {
// Assume that both values being null is an indication that there is no value
// to use. Otherwise, a null current value implies that the user has cleared
// the default value.
return passedInDefaultValue;
}
if (defaultValue != null) {
return defaultValue;
}
return passedInDefaultValue;
return value;
}
public boolean isRegistered() {
@ -97,11 +94,7 @@ public abstract class Option {
public boolean isDefault() {
Object value = getCurrentValue();
if (value == null) {
return true;
}
return value.equals(defaultValue);
return Objects.equals(value, defaultValue);
}
@Override
@ -125,46 +118,12 @@ public abstract class Option {
Throwable throwable = new Throwable();
StackTraceElement[] stackTrace = throwable.getStackTrace();
String information = getInceptionInformationFromTheFirstClassThatIsNotUs(stackTrace);
inceptionInformation = information;
}
private String getInceptionInformationFromTheFirstClassThatIsNotUs(
StackTraceElement[] stackTrace) {
// To find our creation point we can use a simple algorithm: find the name of our class,
// which is in the first stack trace element and then keep walking backwards until that
// name is not ours.
//
String myClassName = getClass().getName();
int myClassNameStartIndex = -1;
for (int i = 1; i < stackTrace.length; i++) { // start at 1, because we are the first item
StackTraceElement stackTraceElement = stackTrace[i];
String elementClassName = stackTraceElement.getClassName();
if (myClassName.equals(elementClassName)) {
myClassNameStartIndex = i;
break;
}
}
// Finally, go backwards until we find a non-options class in the stack, in order
// to remove infrastructure code from the client that called the options API.
int creatorIndex = myClassNameStartIndex;
for (int i = myClassNameStartIndex; i < stackTrace.length; i++) { // start at 1, because we are the first item
StackTraceElement stackTraceElement = stackTrace[i];
String elementClassName = stackTraceElement.getClassName();
if (elementClassName.toLowerCase().indexOf("option") == -1) {
creatorIndex = i;
break;
}
}
return stackTrace[creatorIndex].toString();
StackTraceElement[] filteredTrace =
ReflectionUtilities.filterStackTrace(stackTrace, "option", "OptionsManager");
inceptionInformation = filteredTrace[0].toString();
}
public OptionType getOptionType() {
return optionType;
}
}

View file

@ -41,6 +41,10 @@ import ghidra.util.exception.AssertException;
* The Options Dialog shows the hierarchy in tree format.
*/
public class ToolOptions extends AbstractOptions {
private static final String CLASS_ATTRIBUTE = "CLASS";
private static final String NAME_ATTRIBUTE = "NAME";
private static final String WRAPPED_OPTION_NAME = "WRAPPED_OPTION";
private static final String CLEARED_VALUE_ELEMENT_NAME = "CLEARED_VALUE";
public static final Set<Class<?>> PRIMITIVE_CLASSES = buildPrimitiveClassSet();
public static final Set<Class<?>> WRAPPABLE_CLASSES = buildWrappableClassSet();
@ -81,34 +85,57 @@ public class ToolOptions extends AbstractOptions {
* @param root XML that contains the set of options to restore
*/
public ToolOptions(Element root) {
this(root.getAttributeValue("NAME"));
this(root.getAttributeValue(NAME_ATTRIBUTE));
SaveState saveState = new SaveState(root);
readNonWrappedOptions(saveState);
try {
readWrappedOptions(root);
}
catch (ReflectiveOperationException exc) {
Msg.error(this, "Unexpected Exception: " + exc.getMessage(), exc);
}
}
private void readNonWrappedOptions(SaveState saveState) {
for (String optionName : saveState.getNames()) {
Object object = saveState.getObject(optionName);
Option option =
createUnregisteredOption(optionName, OptionType.getOptionType(object), null);
option.doSetCurrentValue(object); // use doSet versus set so that it is not registered
valueMap.put(optionName, option);
}
}
Iterator<?> iter = root.getChildren("WRAPPED_OPTION").iterator();
while (iter.hasNext()) {
try {
Element elem = (Element) iter.next();
String optionName = elem.getAttributeValue("NAME");
Class<?> c = Class.forName(elem.getAttributeValue("CLASS"));
Constructor<?> constructor = c.getDeclaredConstructor();
WrappedOption wo = (WrappedOption) constructor.newInstance();
wo.readState(new SaveState(elem));
Option option = createUnregisteredOption(optionName, wo.getOptionType(), null);
option.doSetCurrentValue(wo.getObject());// use doSet versus set so that it is not registered
valueMap.put(optionName, option);
private void readWrappedOptions(Element root) throws ReflectiveOperationException {
Iterator<?> it = root.getChildren(WRAPPED_OPTION_NAME).iterator();
while (it.hasNext()) {
Element element = (Element) it.next();
List<?> children = element.getChildren();
if (children.isEmpty()) {
continue; // shouldn't happen
}
catch (Exception exc) {
Msg.error(this, "Unexpected Exception: " + exc.getMessage(), exc);
String optionName = element.getAttributeValue(NAME_ATTRIBUTE);
Class<?> c = Class.forName(element.getAttributeValue(CLASS_ATTRIBUTE));
Constructor<?> constructor = c.getDeclaredConstructor();
WrappedOption wo = (WrappedOption) constructor.newInstance();
Option option = createUnregisteredOption(optionName, wo.getOptionType(), null);
valueMap.put(optionName, option);
Element child = (Element) children.get(0);
String elementName = child.getName();
if (CLEARED_VALUE_ELEMENT_NAME.equals(elementName)) {
// a signal that the default option value has been cleared
option.doSetCurrentValue(null); // use doSet so that it is not registered
}
else {
wo.readState(new SaveState(element));
option.doSetCurrentValue(wo.getObject()); // use doSet so that it is not registered
}
}
}
@ -122,8 +149,20 @@ public class ToolOptions extends AbstractOptions {
* @return the xml root element
*/
public Element getXmlRoot(boolean includeDefaultBindings) {
SaveState saveState = new SaveState(XML_ELEMENT_NAME);
writeNonWrappedOptions(includeDefaultBindings, saveState);
Element root = saveState.saveToXml();
root.setAttribute(NAME_ATTRIBUTE, name);
writeWrappedOptions(includeDefaultBindings, root);
return root;
}
private void writeNonWrappedOptions(boolean includeDefaultBindings, SaveState saveState) {
for (String optionName : valueMap.keySet()) {
Option optionValue = valueMap.get(optionName);
if (includeDefaultBindings || !optionValue.isDefault()) {
@ -133,27 +172,43 @@ public class ToolOptions extends AbstractOptions {
}
}
}
}
Element root = saveState.saveToXml();
root.setAttribute("NAME", name);
private void writeWrappedOptions(boolean includeDefaultBindings, Element root) {
for (String optionName : valueMap.keySet()) {
Option optionValue = valueMap.get(optionName);
if (includeDefaultBindings || !optionValue.isDefault()) {
Object value = optionValue.getValue(null);
if (value != null && !isSupportedBySaveState(value)) {
WrappedOption wrappedOption = wrapOption(value);
SaveState ss = new SaveState("WRAPPED_OPTION");
wrappedOption.writeState(ss);
Element elem = ss.saveToXml();
elem.setAttribute("NAME", optionName);
elem.setAttribute("CLASS", wrappedOption.getClass().getName());
root.addContent(elem);
Option option = valueMap.get(optionName);
if (includeDefaultBindings || !option.isDefault()) {
Object value = option.getCurrentValue();
if (isSupportedBySaveState(value)) {
// handled above
continue;
}
WrappedOption wrappedOption = wrapOption(option);
if (wrappedOption == null) {
// cannot write an option without a value to determine its type
continue;
}
SaveState ss = new SaveState(WRAPPED_OPTION_NAME);
Element elem = null;
if (value == null) {
// Handle the null case ourselves, not using the wrapped option (and when
// reading from xml) so that the logic does not need to in each wrapped option
elem = ss.saveToXml();
elem.addContent(new Element(CLEARED_VALUE_ELEMENT_NAME));
}
else {
wrappedOption.writeState(ss);
elem = ss.saveToXml();
}
elem.setAttribute(NAME_ATTRIBUTE, optionName);
elem.setAttribute(CLASS_ATTRIBUTE, wrappedOption.getClass().getName());
root.addContent(elem);
}
}
return root;
}
private boolean isSupportedBySaveState(Object obj) {
@ -170,7 +225,19 @@ public class ToolOptions extends AbstractOptions {
}
private WrappedOption wrapOption(Object value) {
private WrappedOption wrapOption(Option option) {
Object value = null;
value = option.getCurrentValue();
if (value == null) {
value = option.getDefaultValue();
}
if (value == null) {
// nothing to wrap
return null;
}
if (value instanceof CustomOption) {
return new WrappedCustomOption((CustomOption) value);
}
@ -232,44 +299,6 @@ public class ToolOptions extends AbstractOptions {
}
}
////////////////////////////////////////////////////////////////
private class NotifyListenersRunnable implements Runnable {
private String optionName;
private Object oldValue;
private Object newValue;
private boolean vetoed;
NotifyListenersRunnable(String optionName, Object oldValue, Object newValue) {
this.optionName = optionName;
this.oldValue = oldValue;
this.newValue = newValue;
}
@Override
public void run() {
List<OptionsChangeListener> notifiedListeners = new ArrayList<>();
try {
for (OptionsChangeListener listener : listeners) {
listener.optionsChanged(ToolOptions.this, optionName, oldValue, newValue);
notifiedListeners.add(listener);
}
}
catch (OptionsVetoException e) {
vetoed = true;
for (OptionsChangeListener notifiedListener : notifiedListeners) {
notifiedListener.optionsChanged(ToolOptions.this, optionName, newValue,
oldValue);
}
}
}
public boolean wasVetoed() {
return vetoed;
}
}
/**
* Adds all the options name/value pairs to this Options.
* @param newOptions the new options into which the current options values will be placed
@ -356,6 +385,8 @@ public class ToolOptions extends AbstractOptions {
ToolOption(String name, OptionType type, String description, HelpLocation helpLocation,
Object defaultValue, boolean isRegistered, PropertyEditor editor) {
super(name, type, description, helpLocation, defaultValue, isRegistered, editor);
this.currentValue = defaultValue;
}
@Override
@ -385,7 +416,44 @@ public class ToolOptions extends AbstractOptions {
protected boolean notifyOptionChanged(String optionName, Object oldValue, Object newValue) {
NotifyListenersRunnable runnable =
new NotifyListenersRunnable(optionName, oldValue, newValue);
SystemUtilities.runSwingNow(runnable);
Swing.runNow(runnable);
return !runnable.wasVetoed();
}
private class NotifyListenersRunnable implements Runnable {
private String optionName;
private Object oldValue;
private Object newValue;
private boolean vetoed;
NotifyListenersRunnable(String optionName, Object oldValue, Object newValue) {
this.optionName = optionName;
this.oldValue = oldValue;
this.newValue = newValue;
}
@Override
public void run() {
List<OptionsChangeListener> notifiedListeners = new ArrayList<>();
try {
for (OptionsChangeListener listener : listeners) {
listener.optionsChanged(ToolOptions.this, optionName, oldValue, newValue);
notifiedListeners.add(listener);
}
}
catch (OptionsVetoException e) {
vetoed = true;
for (OptionsChangeListener notifiedListener : notifiedListeners) {
notifiedListener.optionsChanged(ToolOptions.this, optionName, newValue,
oldValue);
}
}
}
public boolean wasVetoed() {
return vetoed;
}
}
}