Changes to ActionBuilder to allow creating actions that work on more

specific ActionContext
This commit is contained in:
dev747368 2020-02-05 17:32:13 -05:00 committed by ghidravore
parent 15468c7b1a
commit d181cec070
6 changed files with 358 additions and 19 deletions

View File

@ -41,8 +41,15 @@ import resources.ResourceManager;
*
* @param <T> The type of DockingAction to build
* @param <B> the Type of action builder
* @param <C> The type of ActionContext. By default, the ActionContext type always starts as
* the base ActionContext class. If the client calls the {@link #withContext(Class)} method on
* the builder, then that class (which must be a subclass of ActionContext) becomes the ActionContext
* type that will be used for future calls to the builder methods that take predicates with
* ActionContext (i.e. {@link #enabledWhen(Predicate)} and {@link #validContextWhen(Predicate)}.
* This works by substituting a builder with a different ActionContext type when chaining after
* the {@link #withContext(Class)} call.
*/
public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends AbstractActionBuilder<T, B>> {
public abstract class AbstractActionBuilder<T extends DockingActionIf, C extends ActionContext, B extends AbstractActionBuilder<T, C, B>> {
/**
* Name for the {@code DockingAction}
@ -54,6 +61,11 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
*/
protected String owner;
/**
* Specifies the type of ActionContext that the built action works on.
*/
protected Class<? extends ActionContext> actionContextClass;
/**
* The {@code KeyBindingType} for this {@code DockingAction}
*/
@ -62,7 +74,7 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
/**
* The callback to perform when the action is invoked
*/
protected Consumer<ActionContext> actionCallback;
protected Consumer<C> actionCallback;
/**
* Description for the {@code DockingAction}. (optional)
@ -147,17 +159,17 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
/**
* Predicate for determining if an action is enabled for a given context
*/
private Predicate<ActionContext> enabledPredicate;
private Predicate<C> enabledPredicate;
/**
* Predicate for determining if an action should be included on the pop-up menu
*/
private Predicate<ActionContext> popupPredicate;
private Predicate<C> popupPredicate;
/**
* Predicate for determining if an action is applicable for a given context
*/
private Predicate<ActionContext> validContextPredicate;
private Predicate<C> validContextPredicate;
/**
* Builder constructor
@ -167,6 +179,7 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
public AbstractActionBuilder(String name, String owner) {
this.name = name;
this.owner = owner;
this.actionContextClass = ActionContext.class;
}
/**
@ -370,7 +383,8 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
* @see #popupMenuGroup(String)
*/
public B popupMenuGroup(String group, String subGroup) {
popupSubGroup = group;
popupGroup = group;
popupSubGroup = subGroup;
return self();
}
@ -446,7 +460,8 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
* @see #toolBarGroup(String)
*/
public B toolBarGroup(String group, String subGroup) {
toolBarSubGroup = group;
toolBarGroup = group;
toolBarSubGroup = subGroup;
return self();
}
@ -484,7 +499,7 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
* @param action the callback to execute when the action is invoked
* @return this builder (for chaining)
*/
public B onAction(Consumer<ActionContext> action) {
public B onAction(Consumer<C> action) {
actionCallback = action;
return self();
}
@ -501,7 +516,7 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
* enabled state
* @return this builder (for chaining)
*/
public B enabledWhen(Predicate<ActionContext> predicate) {
public B enabledWhen(Predicate<C> predicate) {
enabledPredicate = predicate;
return self();
}
@ -524,7 +539,7 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
* @return this builder (for chaining)
* @see #popupMenuPath(String...)
*/
public B popupWhen(Predicate<ActionContext> predicate) {
public B popupWhen(Predicate<C> predicate) {
popupPredicate = predicate;
return self();
}
@ -540,11 +555,68 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
* validity for a given {@link ActionContext}
* @return this builder (for chaining)
*/
public B validContextWhen(Predicate<ActionContext> predicate) {
public B validContextWhen(Predicate<C> predicate) {
validContextPredicate = predicate;
return self();
}
/**
* Sets the specific ActionContext type to use for the various predicate calls
* ({@link #validContextWhen(Predicate)}, {@link #enabledWhen(Predicate)}, and
* {@link #popupWhen(Predicate)}).
* <P>
* In other words, this allows the client to specify the type of ActionContext that is valid for
* the action being built.
* <P>
* To be effective, this method must be called <b>before</b> setting any of the predicates
* such as the {@link #enabledWhen(Predicate)}. Once this method is called you can define your
* predicates using the more specific ActionContext and be assured your predicates will only
* be called when the current action context is the type (or sub-type) of the context you have
* specified here.
* <P>
* For example, assume you have an action that is only enabled when the context is of type
* FooActionContext. If you don't call this method to set the ActionContext type, you would have
* to write your predicate something like this:
* <pre>
* builder.enabledWhen(context -> {
* if (!(context instanceof FooContext)) {
* return false;
* }
* return ((FooContext) context).isAwesome();
* });
* </pre>
* But by first calling the builder method <CODE>withContext(FooContext.class)</CODE>, you can
* simply write:
*
* <pre>
* builder.enabledWhen(context -> return context.isAwesome() }
* </pre>
*
* @param newActionContextClass the more specific ActionContext type.
* @param <AC2> The new ActionContext type (as determined by the newActionContextClass) that
* the returned builder will have.
* @param <B2> the new builder type.
* @return an ActionBuilder whose generic types have been modified to match the new ActionContext.
* It still contains all the configuration that has been applied so far.
*/
@SuppressWarnings("unchecked")
public <AC2 extends ActionContext, B2 extends AbstractActionBuilder<T, AC2, B2>> B2 withContext(
Class<AC2> newActionContextClass) {
// To make this work, we need to return a builder whose ActionContext is AC2 and not AC
// (which is what this builder is now)
//
// Since we "know" that the only thing that matters regarding the ActionContext type is that
// the template type (AC) must match the type of actionContextClass instance variable, we
// can get away with returning this same builder and casting it to be a builder with type
// AC2 instead of AC. We can do this since we set the actionContextClass below
actionContextClass = newActionContextClass;
B2 newSelf = (B2) self();
return newSelf;
}
protected void validate() {
if (actionCallback == null) {
throw new IllegalStateException(
@ -566,16 +638,36 @@ public abstract class AbstractActionBuilder<T extends DockingActionIf, B extends
}
if (enabledPredicate != null) {
action.enabledWhen(enabledPredicate);
action.enabledWhen(adaptPredicate(enabledPredicate));
}
if (validContextPredicate != null) {
action.validContextWhen(validContextPredicate);
action.validContextWhen(adaptPredicate(validContextPredicate));
}
if (popupPredicate != null) {
action.popupWhen(enabledPredicate);
action.popupWhen(adaptPredicate(popupPredicate));
}
}
/**
* Since the built action will need a predicate that handles any action type, this method
* creates a predicate that adapts a user supplied predicate for a more specific ActionContext
* to a general predicate that can accept any ActionContext.
* @param predicate the client supplied predicate that expects a more specific ActionContext
* @return a predicate that can handle any ActionContext
*/
@SuppressWarnings("unchecked")
private Predicate<ActionContext> adaptPredicate(Predicate<C> predicate) {
if (actionContextClass == ActionContext.class) {
// don't wrap the predicate if it doesn't need it
return (Predicate<ActionContext>) predicate;
}
// Convert a sub-classed ActionContext predicate to a plain ActionContext predicate
Predicate<ActionContext> predicateAdapter = (ac) -> {
return actionContextClass.isInstance(ac) && predicate.test((C) ac);
};
return predicateAdapter;
}
protected boolean isPopupAction() {
return popupPath != null;
}

View File

@ -21,7 +21,7 @@ import docking.action.DockingAction;
* Builder for {@link DockingAction}s
*/
public class ActionBuilder
extends AbstractActionBuilder<DockingAction, ActionBuilder> {
extends AbstractActionBuilder<DockingAction, ActionContext, ActionBuilder> {
/**
* Builder constructor

View File

@ -26,7 +26,7 @@ import docking.menu.MultiActionDockingAction;
* Builder for {@link MultiActionDockingAction}
*/
public class MultiActionBuilder
extends AbstractActionBuilder<MultiActionDockingAction, MultiActionBuilder> {
extends AbstractActionBuilder<MultiActionDockingAction, ActionContext, MultiActionBuilder> {
/**
* List of actions for the the MultActionDockingAction
*/

View File

@ -27,7 +27,7 @@ import docking.widgets.EventTrigger;
* @param <T> The action state type
*/
public class MultiStateActionBuilder<T> extends
AbstractActionBuilder<MultiStateDockingAction<T>, MultiStateActionBuilder<T>> {
AbstractActionBuilder<MultiStateDockingAction<T>, ActionContext, MultiStateActionBuilder<T>> {
private BiConsumer<ActionState<T>, EventTrigger> actionStateChangedCallback;
private boolean performActionOnButtonClick;
@ -77,7 +77,7 @@ public class MultiStateActionBuilder<T> extends
public MultiStateDockingAction<T> build() {
validate();
MultiStateDockingAction<T> action =
new MultiStateDockingAction<T>(name, owner, isToolbarAction()) {
new MultiStateDockingAction<>(name, owner, isToolbarAction()) {
@Override
public void actionStateChanged(ActionState<T> newActionState,

View File

@ -21,7 +21,7 @@ import docking.action.ToggleDockingAction;
* Builder for {@link ToggleDockingAction}s
*/
public class ToggleActionBuilder extends
AbstractActionBuilder<ToggleDockingAction, ToggleActionBuilder> {
AbstractActionBuilder<ToggleDockingAction, ActionContext, ToggleActionBuilder> {
/**
* The initial toggle state for the action

View File

@ -0,0 +1,247 @@
/* ###
* 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.action;
import static org.junit.Assert.*;
import javax.swing.KeyStroke;
import org.junit.Test;
import docking.ActionContext;
import docking.action.builder.ActionBuilder;
import resources.Icons;
public class ActionBuilderTest {
private int actionCount = 0;
@Test
public void testDescription() {
DockingAction action = new ActionBuilder("Test", "Test")
.description("foo")
.onAction(e -> actionCount++)
.build();
assertEquals("foo", action.getDescription());
}
@Test
public void testMenuPath() {
DockingAction action = new ActionBuilder("Test", "Test")
.menuPath("foo", "bar")
.onAction(e -> actionCount++)
.build();
MenuData data = action.getMenuBarData();
assertEquals("foo->bar", data.getMenuPathAsString());
}
@Test
public void testMenuGroup() {
DockingAction action = new ActionBuilder("Test", "Test")
.menuPath("foo", "bar")
.menuGroup("A", "B")
.onAction(e -> actionCount++)
.build();
MenuData data = action.getMenuBarData();
assertEquals("A", data.getMenuGroup());
assertEquals("B", data.getMenuSubGroup());
}
@Test
public void testMenuIcon() {
DockingAction action = new ActionBuilder("Test", "Test")
.menuPath("foo", "bar")
.menuIcon(Icons.ADD_ICON)
.onAction(e -> actionCount++)
.build();
MenuData data = action.getMenuBarData();
assertEquals(Icons.ADD_ICON, data.getMenuIcon());
}
@Test
public void testMenuMnemonic() {
DockingAction action = new ActionBuilder("Test", "Test")
.menuPath("foo", "bar")
.menuMnemonic(5)
.onAction(e -> actionCount++)
.build();
MenuData data = action.getMenuBarData();
assertEquals(5, data.getMnemonic());
}
@Test
public void testPopupPath() {
DockingAction action = new ActionBuilder("Test", "Test")
.popupMenuPath("foo", "bar")
.onAction(e -> actionCount++)
.build();
MenuData data = action.getPopupMenuData();
assertEquals("foo->bar", data.getMenuPathAsString());
}
@Test
public void testPopupGroup() {
DockingAction action = new ActionBuilder("Test", "Test")
.popupMenuPath("foo", "bar")
.popupMenuGroup("A", "B")
.onAction(e -> actionCount++)
.build();
MenuData data = action.getPopupMenuData();
assertEquals("A", data.getMenuGroup());
assertEquals("B", data.getMenuSubGroup());
}
@Test
public void testPopupIcon() {
DockingAction action = new ActionBuilder("Test", "Test")
.popupMenuPath("foo", "bar")
.popupMenuIcon(Icons.ADD_ICON)
.onAction(e -> actionCount++)
.build();
MenuData data = action.getPopupMenuData();
assertEquals(Icons.ADD_ICON, data.getMenuIcon());
}
@Test
public void testToolbarIcon() {
DockingAction action = new ActionBuilder("Test", "Test")
.toolBarIcon(Icons.ADD_ICON)
.onAction(e -> actionCount++)
.build();
ToolBarData data = action.getToolBarData();
assertEquals(Icons.ADD_ICON, data.getIcon());
}
@Test
public void testToolbarGroup() {
DockingAction action = new ActionBuilder("Test", "Test")
.toolBarIcon(Icons.ADD_ICON)
.toolBarGroup("A", "B")
.onAction(e -> actionCount++)
.build();
ToolBarData data = action.getToolBarData();
assertEquals("A", data.getToolBarGroup());
assertEquals("B", data.getToolBarSubGroup());
}
@Test
public void testKeyBindingKeyStroke() {
DockingAction action = new ActionBuilder("Test", "Test")
.keyBinding(KeyStroke.getKeyStroke("A"))
.onAction(e -> actionCount++)
.build();
assertEquals(KeyStroke.getKeyStroke("A"), action.getKeyBinding());
}
@Test
public void testKeyBindingKeyString() {
DockingAction action = new ActionBuilder("Test", "Test")
.keyBinding("ALT A")
.onAction(e -> actionCount++)
.build();
assertEquals(KeyStroke.getKeyStroke("ALT A"), action.getKeyBinding());
}
@Test
public void testOnAction() {
DockingAction action = new ActionBuilder("Test", "Test")
.onAction(e -> actionCount = 6)
.build();
assertEquals(0, actionCount);
action.actionPerformed(new ActionContext());
assertEquals(6, actionCount);
}
@Test
public void testEnabled() {
DockingAction action = new ActionBuilder("Test", "Test")
.enabled(true)
.onAction(e -> actionCount++)
.build();
assertTrue(action.isEnabled());
action = new ActionBuilder("Test", "Test")
.enabled(false)
.onAction(e -> actionCount++)
.build();
assertFalse(action.isEnabled());
}
@Test
public void testEnabledWhen() {
DockingAction action = new ActionBuilder("Test", "Test")
.enabledWhen(c -> c.getContextObject() == this)
.onAction(e -> actionCount++)
.build();
assertTrue(action.isEnabledForContext(new ActionContext(null, this, null)));
assertFalse(action.isEnabledForContext(new ActionContext()));
}
@Test
public void testValidContextWhen() {
DockingAction action = new ActionBuilder("Test", "Test")
.validContextWhen(c -> c.getContextObject() == this)
.onAction(e -> actionCount++)
.build();
assertTrue(action.isValidContext(new ActionContext(null, this, null)));
assertFalse(action.isValidContext(new ActionContext()));
}
@Test
public void testPopupWhen() {
DockingAction action = new ActionBuilder("Test", "Test")
.popupWhen(c -> c.getContextObject() == this)
.onAction(e -> actionCount++)
.build();
assertTrue(action.isAddToPopup(new ActionContext(null, this, null)));
assertFalse(action.isAddToPopup(new ActionContext()));
}
@Test
public void testWithContext() {
DockingAction action = new ActionBuilder("Test", "Test")
.withContext(FooActionContext.class)
.enabledWhen(c -> c.foo())
.onAction(e -> actionCount++)
.build();
assertFalse(action.isEnabledForContext(new ActionContext()));
assertTrue(action.isEnabledForContext(new FooActionContext()));
}
static class FooActionContext extends ActionContext {
public boolean foo() {
return true;
}
}
}