GT-2925 - Key Bindings - Support Window Menu Provider Key Bindings -

Step 6 - added action to 'Go To Last Provider'; cleanup for review
This commit is contained in:
dragonmacher 2019-06-28 07:56:40 -04:00
parent 115243801e
commit d684ee3ce6
7 changed files with 340 additions and 53 deletions

View file

@ -0,0 +1,103 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.navigation;
import java.awt.event.KeyEvent;
import java.util.function.Consumer;
import javax.swing.KeyStroke;
import docking.*;
import docking.action.*;
import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.plugintool.util.ToolConstants;
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = CorePluginPackage.NAME,
category = PluginCategoryNames.NAVIGATION,
shortDescription = "Component Provider Navigation",
description = "The plugin provides actions to manage switching between Component Providers."
)
//@formatter:on
public class ProviderNavigationPlugin extends Plugin {
static final String GO_TO_LAST_ACTIVE_COMPONENT_ACTION_NAME = "Go To Last Active Component";
private ComponentProvider previousActiveProvider;
private ComponentProvider currentActiveProvider;
private Consumer<ComponentProvider> providerActivator =
provider -> tool.showComponentProvider(provider, true);
private DockingContextListener contextListener = context -> {
ComponentProvider componentProvider = context.getComponentProvider();
if (componentProvider != null) {
if (componentProvider != currentActiveProvider) {
previousActiveProvider = currentActiveProvider;
currentActiveProvider = componentProvider;
}
}
};
public ProviderNavigationPlugin(PluginTool tool) {
super(tool);
createActions();
tool.addContextListener(contextListener);
}
private void createActions() {
DockingAction previousProviderAction =
new DockingAction(GO_TO_LAST_ACTIVE_COMPONENT_ACTION_NAME, getName()) {
@Override
public void actionPerformed(ActionContext context) {
providerActivator.accept(previousActiveProvider);
}
@Override
public boolean isEnabledForContext(ActionContext context) {
return previousActiveProvider != null;
}
};
previousProviderAction.setMenuBarData(new MenuData(
new String[] { ToolConstants.MENU_NAVIGATION, GO_TO_LAST_ACTIVE_COMPONENT_ACTION_NAME },
null, ToolConstants.MENU_NAVIGATION_GROUP_WINDOWS, MenuData.NO_MNEMONIC,
"xLowInMenuSubGroup"));
previousProviderAction.setKeyBindingData(new KeyBindingData(
KeyStroke.getKeyStroke(KeyEvent.VK_F6, DockingUtils.CONTROL_KEY_MODIFIER_MASK)));
tool.addAction(previousProviderAction);
}
// for testing
void resetTrackingState() {
previousActiveProvider = null;
currentActiveProvider = null;
}
// for testing
void setProviderActivator(Consumer<ComponentProvider> newActivator) {
this.providerActivator = newActivator;
}
}

View file

@ -15,6 +15,15 @@
*/
package ghidra.app.plugin.core.progmgr;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import docking.ActionContext;
import docking.DockingUtils;
import docking.action.*;
import ghidra.app.CorePluginPackage;
import ghidra.app.events.*;
import ghidra.app.plugin.PluginCategoryNames;
@ -23,18 +32,10 @@ import ghidra.app.services.ProgramManager;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.plugintool.util.ToolConstants;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
import java.awt.event.*;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import docking.ActionContext;
import docking.DockingUtils;
import docking.action.*;
/**
* Plugin to show a "tab" for each open program; the selected tab is the activated program.
*/
@ -58,10 +59,10 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
// DockingUtils calls into Swing code. Further, we don't want Swing code being accessed
// when the Plugin classes are loaded, as they get loaded in the headless environment.
//
private final KeyStroke NEXT_TAB_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_F9,
DockingUtils.CONTROL_KEY_MODIFIER_MASK);
private final KeyStroke PREVIOUS_TAB_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_F8,
DockingUtils.CONTROL_KEY_MODIFIER_MASK);
private final KeyStroke NEXT_TAB_KEYSTROKE =
KeyStroke.getKeyStroke(KeyEvent.VK_F9, DockingUtils.CONTROL_KEY_MODIFIER_MASK);
private final KeyStroke PREVIOUS_TAB_KEYSTROKE =
KeyStroke.getKeyStroke(KeyEvent.VK_F8, DockingUtils.CONTROL_KEY_MODIFIER_MASK);
private MultiTabPanel tabPanel;
private ProgramManager progService;
@ -92,14 +93,17 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
showProgramList();
}
};
goToProgramAction.setMenuBarData(new MenuData(new String[] { "Navigation",
"Go To Program..." }, null, "GoToProgram", MenuData.NO_MNEMONIC, firstGroup));
goToProgramAction.setKeyBindingData(new KeyBindingData(KeyEvent.VK_F7,
InputEvent.CTRL_DOWN_MASK));
goToProgramAction.setMenuBarData(
new MenuData(new String[] { ToolConstants.MENU_NAVIGATION, "Go To Program..." }, null,
ToolConstants.MENU_NAVIGATION_GROUP_WINDOWS, MenuData.NO_MNEMONIC, firstGroup));
goToProgramAction.setKeyBindingData(
new KeyBindingData(KeyEvent.VK_F7, InputEvent.CTRL_DOWN_MASK));
goToProgramAction.setEnabled(false);
goToProgramAction.setDescription("Shows the program selection dialog with the current program selected");
goToProgramAction.setHelpLocation(new HelpLocation("ProgramManagerPlugin", "Go_To_Program"));
goToProgramAction.setDescription(
"Shows the program selection dialog with the current program selected");
goToProgramAction.setHelpLocation(
new HelpLocation("ProgramManagerPlugin", "Go_To_Program"));
goToNextProgramAction = new DockingAction("Go To Next Program", getName()) {
@Override
@ -109,10 +113,11 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
}
};
goToNextProgramAction.setEnabled(false);
goToNextProgramAction.setDescription("Highlights the next program tab and then switches to that program");
goToNextProgramAction.setDescription(
"Highlights the next program tab and then switches to that program");
goToNextProgramAction.setKeyBindingData(new KeyBindingData(NEXT_TAB_KEYSTROKE));
goToNextProgramAction.setHelpLocation(new HelpLocation("ProgramManagerPlugin",
"Go_To_Next_And_Previous_Program"));
goToNextProgramAction.setHelpLocation(
new HelpLocation("ProgramManagerPlugin", "Go_To_Next_And_Previous_Program"));
goToPreviousProgramAction = new DockingAction("Go To Previous Program", getName()) {
@Override
@ -122,20 +127,14 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
}
};
goToPreviousProgramAction.setEnabled(false);
goToPreviousProgramAction.setMenuBarData(new MenuData(new String[] { "Navigation" }, null,
null));
goToPreviousProgramAction.setKeyBindingData(new KeyBindingData(PREVIOUS_TAB_KEYSTROKE));
goToPreviousProgramAction.setDescription("Highlights the previous program tab and then switches to that program");
goToPreviousProgramAction.setHelpLocation(new HelpLocation("ProgramManagerPlugin",
"Go_To_Next_And_Previous_Program"));
goToPreviousProgramAction.setDescription(
"Highlights the previous program tab and then switches to that program");
goToPreviousProgramAction.setHelpLocation(
new HelpLocation("ProgramManagerPlugin", "Go_To_Next_And_Previous_Program"));
// this timer is to give the user time to select successive programs before activating one
selectHighlightedProgramTimer = new Timer(750, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
selectHighlightedProgram();
}
});
selectHighlightedProgramTimer = new Timer(750, e -> selectHighlightedProgram());
selectHighlightedProgramTimer.setRepeats(false);
goToLastActiveProgramAction = new DockingAction("Go To Last Active Program", getName()) {
@ -144,14 +143,16 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
switchToProgram(lastActiveProgram);
}
};
goToLastActiveProgramAction.setMenuBarData(new MenuData(new String[] { "Navigation",
"Go To Last Active Program" }, null, "GoToProgram", MenuData.NO_MNEMONIC, secondGroup));
goToLastActiveProgramAction.setKeyBindingData(new KeyBindingData(KeyEvent.VK_F6,
InputEvent.CTRL_DOWN_MASK));
goToLastActiveProgramAction.setMenuBarData(new MenuData(
new String[] { ToolConstants.MENU_NAVIGATION, "Go To Last Active Program" }, null,
ToolConstants.MENU_NAVIGATION_GROUP_WINDOWS, MenuData.NO_MNEMONIC, secondGroup));
goToLastActiveProgramAction.setKeyBindingData(
new KeyBindingData(KeyEvent.VK_F6, InputEvent.CTRL_DOWN_MASK));
goToLastActiveProgramAction.setEnabled(false);
goToLastActiveProgramAction.setDescription("Activates the last program used before the current program");
goToLastActiveProgramAction.setHelpLocation(new HelpLocation("ProgramManagerPlugin",
"Go_To_Last_Active_Program"));
goToLastActiveProgramAction.setDescription(
"Activates the last program used before the current program");
goToLastActiveProgramAction.setHelpLocation(
new HelpLocation("ProgramManagerPlugin", "Go_To_Last_Active_Program"));
tool.addAction(goToProgramAction);
tool.addAction(goToLastActiveProgramAction);

View file

@ -0,0 +1,175 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.navigation;
import static org.junit.Assert.*;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import org.junit.Before;
import org.junit.Test;
import docking.*;
import docking.action.DockingActionIf;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.model.listing.Program;
import ghidra.test.AbstractProgramBasedTest;
import ghidra.util.Msg;
import ghidra.util.datastruct.WeakSet;
public class ProviderNavigationPluginTest extends AbstractProgramBasedTest {
private ProviderNavigationPlugin plugin;
private DockingActionIf previousProviderAction;
private SpyProviderActivator spyProviderActivator = new SpyProviderActivator();
private Set<DockingContextListener> testContextListeners;
@Before
public void setUp() throws Exception {
initialize();
plugin = env.getPlugin(ProviderNavigationPlugin.class);
previousProviderAction =
getAction(tool, ProviderNavigationPlugin.GO_TO_LAST_ACTIVE_COMPONENT_ACTION_NAME);
fakeOutContextNotification();
}
@SuppressWarnings("unchecked")
private void fakeOutContextNotification() {
//
// This is the mechanism the tool uses for notifying clients of Component Provider
// activation. This activation is focus-sensitive, which makes it unreliable for testing.
// Thus, replace this mechanism with one that we can control.
//
DockingWindowManager windowManager = tool.getWindowManager();
testContextListeners = new HashSet<>();
WeakSet<DockingContextListener> contextListeners =
(WeakSet<DockingContextListener>) getInstanceField("contextListeners", windowManager);
testContextListeners.addAll(contextListeners.values());
contextListeners.clear();
//
// Now, install a spy that allows us to know when our action under test triggers and
// with which state it does so.
//
plugin.setProviderActivator(spyProviderActivator);
}
@Override
protected Program getProgram() throws Exception {
return buildProgram();
}
private Program buildProgram() throws Exception {
ProgramBuilder builder = new ProgramBuilder("Test", ProgramBuilder._TOY);
builder.createMemory(".text", "0x1001000", 0x6600);
return builder.getProgram();
}
@Test
public void testGoToLastActiveComponent() {
clearPluginState();
assertPreviousProviderActionNotEnabled();
ComponentProvider bookmarks = activateProvider("Bookmarks");
assertPreviousProviderActionNotEnabled(); // first provider; nothing to go back to
ComponentProvider dataTypes = activateProvider("DataTypes Provider");
assertPreviousProviderActionEnabled();
// active provider : 'data types'; previous: 'bookmarks'
performPreviousProviderAction();
assertActivated(bookmarks);
// active provider : 'bookmarks'; previous: 'data types'
performPreviousProviderAction();
assertActivated(dataTypes);
activateProvider("Symbol Table");
// active provider : 'symbol table'; previous: 'data types'
performPreviousProviderAction();
assertActivated(dataTypes);
}
private void clearPluginState() {
waitForSwing();
runSwing(() -> plugin.resetTrackingState());
}
private void assertActivated(ComponentProvider bookmarks) {
assertEquals("The active provider was not restored correctly", bookmarks,
spyProviderActivator.lastActivated);
}
private void performPreviousProviderAction() {
performAction(previousProviderAction, true);
waitForSwing();
}
private void assertPreviousProviderActionEnabled() {
assertTrue(
"'" + ProviderNavigationPlugin.GO_TO_LAST_ACTIVE_COMPONENT_ACTION_NAME + "'" +
" should be enabled when there is a previous provider set",
previousProviderAction.isEnabledForContext(new ActionContext()));
}
private void assertPreviousProviderActionNotEnabled() {
assertFalse(
"'" + ProviderNavigationPlugin.GO_TO_LAST_ACTIVE_COMPONENT_ACTION_NAME + "'" +
" should not be enabled when there is no previous provider set",
previousProviderAction.isEnabledForContext(new ActionContext()));
}
private ComponentProvider activateProvider(String name) {
waitForSwing();
ComponentProvider provider = tool.getComponentProvider(name);
assertNotNull(provider);
tool.showComponentProvider(provider, true);
runSwing(() -> forceActivate(provider));
waitForSwing();
return provider;
}
private void forceActivate(ComponentProvider provider) {
ActionContext context = new ActionContext(provider, provider);
for (DockingContextListener l : testContextListeners) {
l.contextChanged(context);
}
}
private class SpyProviderActivator implements Consumer<ComponentProvider> {
private ComponentProvider lastActivated;
@Override
public void accept(ComponentProvider c) {
Msg.out("Spy - activated: " + c);
lastActivated = c;
forceActivate(c);
}
}
}

View file

@ -551,8 +551,6 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext
4) Wire default 'close' action to keybinding
5) Add global action for (show last provider)
--Navigation menu?
6) Revisit all uses of the key binding managed constructor
7) Update table popup actions to be managed
8) Update help locations
Questions:

View file

@ -87,13 +87,12 @@ public class DockableHeader extends GenericHeader
private boolean isDocking;
private Animator focusAnimator;
private int focusToggle = -1;
/**
* Constructs a new DockableHeader for the given dockableComponent.
*
* @param dockableComp
* the dockableComponent that this header is for.
* @param dockableComp the dockableComponent that this header is for.
* @param isDocking true means this widget can be dragged and docked by the user
*/
DockableHeader(DockableComponent dockableComp, boolean isDocking) {
this.dockComp = dockableComp;
@ -173,8 +172,11 @@ public class DockableHeader extends GenericHeader
}
protected Animator createEmphasizingAnimator(JFrame parentFrame) {
focusToggle += 1;
switch (focusToggle) {
double random = Math.random();
int choices = 4;
int value = (int) (choices * random);
switch (value) {
case 0:
return AnimationUtils.shakeComponent(component);
case 1:
@ -182,7 +184,6 @@ public class DockableHeader extends GenericHeader
case 2:
return raiseComponent(parentFrame);
default:
focusToggle = -1;
return AnimationUtils.pulseComponent(component);
}
}
@ -226,12 +227,12 @@ public class DockableHeader extends GenericHeader
if (!isDocking) {
return;
}
// check input event: if any button other than MB1 is pressed,
// don't attempt to process the drag and drop event.
// if any button other than MB1 is pressed, don't attempt to process the drag and drop event
InputEvent ie = event.getTriggerEvent();
int modifiers = ie.getModifiers();
if ((modifiers & InputEvent.BUTTON2_MASK) != 0 ||
(modifiers & InputEvent.BUTTON3_MASK) != 0) {
int modifiers = ie.getModifiersEx();
if ((modifiers & InputEvent.BUTTON2_DOWN_MASK) != 0 ||
(modifiers & InputEvent.BUTTON3_DOWN_MASK) != 0) {
return;
}
DockableComponent.DROP_CODE = DockableComponent.DropCode.WINDOW;

View file

@ -53,6 +53,9 @@ public abstract class WeakSet<T> implements Iterable<T> {
return;
}
// Note: sadly, this code does not work with labmda's, as we cannot get the enclosing
// method/constructor
Class<? extends Object> clazz = t.getClass();
if (!clazz.isAnonymousClass()) {
return; // O.K.

View file

@ -34,6 +34,12 @@ public interface ToolConstants extends DockingToolConstants {
* Used when placing a PluginAction in the "Navigation" menu of the tool.
*/
String MENU_NAVIGATION = "&Navigation";
/**
* Group name for actions to navigate between windows
*/
String MENU_NAVIGATION_GROUP_WINDOWS = "GoToWindow";
/**
* Used when placing a PluginAction in the "Search" menu of the tool.
*/