/* ### * 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; import static org.junit.Assert.*; import java.awt.event.KeyEvent; import java.util.Set; import javax.swing.*; import org.junit.*; import docking.action.*; import docking.actions.KeyEntryDialog; import docking.actions.ToolActions; import docking.tool.util.DockingToolConstants; import generic.theme.GIcon; import ghidra.framework.options.ActionTrigger; import ghidra.framework.options.ToolOptions; import ghidra.framework.plugintool.PluginTool; import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.TestEnv; import ghidra.util.*; import resources.Icons; public class ComponentProviderActionsTest extends AbstractGhidraHeadedIntegrationTest { // note: this has to happen after the test framework is initialized, so it cannot be static private final Icon ICON = Icons.REFRESH_ICON; private static final String PROVIDER_NAME = "Test Action Provider"; private static final KeyStroke CONTROL_T = KeyStroke.getKeyStroke(KeyEvent.VK_T, DockingUtils.CONTROL_KEY_MODIFIER_MASK); private TestEnv env; private PluginTool tool; private TestActionsComponentProvider provider; private SpyErrorLogger spyLogger = new SpyErrorLogger(); @Before public void setUp() throws Exception { env = new TestEnv(); tool = env.showTool(); //tool = env.launchDefaultTool(); provider = new TestActionsComponentProvider(tool); Msg.setErrorLogger(spyLogger); } @After public void tearDown() { env.dispose(); } @Test public void testIcon_WithIcon_BeforeAddedToTool() { setIcon(ICON); showProvider(); assertWindowMenuActionHasIcon(ICON); } @Test public void testIcon_WithIcon_AfterAddedToTool() { setIcon(null); showProvider(); assertWindowMenuActionHasIcon(Icons.EMPTY_ICON); setIcon(ICON); assertWindowMenuActionHasIcon(ICON); } @Test public void testIcon_WithoutIcon() { showProvider(); assertWindowMenuActionHasIcon(Icons.EMPTY_ICON); } @Test public void testSetKeyBinding_DirectlyOnProvider() { // // This is how clients set key bindings on providers, as desired, when constructing them // KeyStroke defaultKs = CONTROL_T; setDefaultKeyBinding(defaultKs); showProvider(); assertProviderKeyStroke(defaultKs); assertOptionsKeyStroke(defaultKs); assertMenuItemHasKeyStroke(defaultKs); } @Test public void testSetKeyBinding_DirectlyOnProvider_TransientProvider() { // // Transient providers cannot have key bindings // switchToTransientProvider(); setErrorsExpected(true); setDefaultKeyBinding(CONTROL_T); setErrorsExpected(false); showProvider(); spyLogger.assertLogMessage("Transient", "cannot", "key", "binding"); } @Test public void testSetTransientAfterSettingKeyBinding() { setDefaultKeyBinding(CONTROL_T); setErrorsExpected(true); switchToTransientProvider(); setErrorsExpected(false); showProvider(); spyLogger.assertLogMessage("Transient", "not", "key", "binding"); } @Test(expected = IllegalStateException.class) public void testSetTransientAfterAddedProviderToTheTool() { showProvider(); switchToTransientProvider(); // exception } @Test public void testSetKeyBinding_ViaDialog_FromWindowMenu() { showProvider(); KeyStroke newKs = CONTROL_T; setKeyBindingViaF4Dialog_FromWindowsMenu(newKs); assertProviderKeyStroke(newKs); assertOptionsKeyStroke(newKs); assertMenuItemHasKeyStroke(newKs); } @Test public void testSetKeyBinding_ViaDialog_FromWindowMenu_ThenFireKeyEventToShowProvider() { showProvider(); KeyStroke newKs = CONTROL_T; setKeyBindingViaF4Dialog_FromWindowsMenu(newKs); hideProvider(); triggerKey(tool.getToolFrame(), CONTROL_T); assertProviderIsActive(); } @Test public void testSetKeyBinding_ViaDialog_FromWindowMenu_ToAlreadyBoundAction() { // // Test the conflicting key binding use case // HasDefaultKeyBindingComponentProvider otherProvider = new HasDefaultKeyBindingComponentProvider(tool); otherProvider.setVisible(true); showProvider(); KeyStroke newKs = CONTROL_T; applyBindingToDialog_FromWindowsMenu(newKs); assertCollisionsWithKeyStroke(); } @Test public void testSetKeyBinding_ViaOptions_WithoutToolbarAction() { showProvider(); KeyStroke newKs = CONTROL_T; setOptionsKeyStroke(newKs); assertProviderKeyStroke(newKs); assertOptionsKeyStroke(newKs); assertMenuItemHasKeyStroke(newKs); assertNoToolbarAction(); } @Test public void testSetKeyBinding_ViaOptions_WithToolbarAction() { setToolbarIcon(ICON); showProvider(); KeyStroke newKs = CONTROL_T; setOptionsKeyStroke(newKs); assertProviderKeyStroke(newKs); assertOptionsKeyStroke(newKs); assertMenuItemHasKeyStroke(newKs); assertToolbarAction(); } @Test public void testSetKeyBinding_ViaDialog_FromToolBar() { setToolbarIcon(ICON); showProvider(); KeyStroke newKs = CONTROL_T; setKeyBindingViaF4Dialog_FromToolToolbar(newKs); assertProviderKeyStroke(newKs); assertOptionsKeyStroke(newKs); assertMenuItemHasKeyStroke(newKs); } @Test public void testSetKeyBinding_TransientProvider_CannotBeSetFromWindowMenu() { switchToTransientProvider(); showProvider(); assertCannotShowKeyBindingDialog_FromWindowsMenu(); } @Test public void testSetKeyBinding_TransientProvider_CannotBeSetFromToolbar() { switchToTransientProvider(); setErrorsExpected(true); setToolbarIcon(ICON); setErrorsExpected(false); spyLogger.assertLogMessage("Transient", "not", "toolbar"); } @Test public void testDefaultKeyBindingAppearsInWindowMenu() { setDefaultKeyBinding(CONTROL_T); showProvider(); assertWindowMenuActionHasKeyBinding(CONTROL_T); } @Test public void testAddToToolbar_WithoutIcon() { runSwing(() -> provider.addToToolbar()); try { setErrorsExpected(true); runSwingWithException(this::showProvider); setErrorsExpected(false); fail(); } catch (Throwable t) { // good } } @Test public void testSetIcon_NullIconWithToolbarAction() { setIcon(ICON); runSwing(() -> provider.addToToolbar()); showProvider(); try { setErrorsExpected(true); runSwingWithException(() -> provider.setIcon(null)); setErrorsExpected(false); fail("Expected an exception passing a null icon when specifying a toolbar action"); } catch (Throwable t) { // expected } } @Test public void testSetIcon_WithToolbarAction() { setToolbarIcon(ICON); showProvider(); assertWindowMenuActionHasIcon(ICON); assertToolbarActionHasIcon(ICON); } @Test public void testSetIcon_WithToolbarAction_AfterActionHasBeenAddedToToolbar() { // // We currently do not prevent providers from changing their icons. Make sure we respond // to changes correctly. // setToolbarIcon(ICON); showProvider(); Icon newIcon = Icons.COLLAPSE_ALL_ICON; setIcon(newIcon); assertWindowMenuActionHasIcon(newIcon); assertToolbarActionHasIcon(newIcon); } @Test public void testSetCloseButtonKeyBinding() { showProvider(); KeyStroke controlEsc = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, DockingUtils.CONTROL_KEY_MODIFIER_MASK); setKeyBindingViaF4Dialog_FromCloseButton(controlEsc); // Note: there may be a test focus issue here. If this test fails sporadically due to // how the action context is generated (it depends on focus). It is only useful to fail // here in development mode. triggerKey(provider.getComponent(), controlEsc); assertProviderIsHidden_InNonBatchMode(); } @Test public void testEveryCloseButtonsActionContext() { // grab the tool that has all the provider's loaded env.closeTool(tool); tool = env.launchDefaultTool(); DockingWindowManager dwm = tool.getWindowManager(); PlaceholderManager pm = dwm.getPlaceholderManager(); Set allProviders = pm.getActiveProviders(); for (ComponentProvider cp : allProviders) { ActionContext context = cp.getActionContext(null); if (context == null) { continue; // a null context is allowed } // this is a rule we need for context to work as intended for some actions (like the // close button) assertSame("Component provider did not make itself the context's provider", context.getComponentProvider(), cp); } } //================================================================================================== // Private Methods //================================================================================================== private void switchToTransientProvider() { provider.setTransient(); } private void showProvider() { provider.addToTool(); tool.showComponentProvider(provider, true); waitForSwing(); waitForCondition(() -> provider.isVisible()); } private void hideProvider() { tool.showComponentProvider(provider, false); waitForSwing(); } private void setDefaultKeyBinding(KeyStroke defaultKs) { runSwing(() -> provider.setKeyBinding(new KeyBindingData(defaultKs))); } private void setIcon(Icon icon) { runSwing(() -> provider.setIcon(icon)); } private void setToolbarIcon(Icon icon) { runSwing(() -> { provider.setIcon(icon); provider.addToToolbar(); }); } private DockingActionIf getShowProviderAction() { DockingActionIf showProviderAction = getAction(tool, provider.getOwner(), provider.getName()); assertNotNull("Could not find action to show ", showProviderAction); return showProviderAction; } private DockingActionIf getWindowMenuShowProviderAction() { waitForSwing(); DockingActionIf action = waitFor(() -> { Set actions = getActionsByName(tool, PROVIDER_NAME); //@formatter:off return actions .stream() .filter(a -> a.getOwner().equals(DockingWindowManager.DOCKING_WINDOWS_OWNER)) .findFirst() .orElseGet(() -> null) ; //@formatter:on }); assertNotNull("Window menu action not installed for provider", action); assertTrue(action.getClass().getSimpleName().contains("ShowComponentAction")); return action; } private DockingActionIf getToolbarShowProviderAction() { DockingWindowManager dwm = tool.getWindowManager(); ActionToGuiMapper guiActions = (ActionToGuiMapper) getInstanceField("actionToGuiMapper", dwm); GlobalMenuAndToolBarManager toolbarManager = (GlobalMenuAndToolBarManager) getInstanceField("menuAndToolBarManager", guiActions); DockingActionIf action = provider.getShowProviderAction(); DockingActionIf toolbarAction = toolbarManager.getToolbarAction(action.getName()); return toolbarAction; } private void setOptionsKeyStroke(KeyStroke newKs) { ToolOptions keyOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS); // shared option name/format: "Provider Name (Shared)" - the shared action's owner is the Tool runSwing(() -> keyOptions.setActionTrigger(provider.getName() + " (Shared)", new ActionTrigger(newKs))); waitForSwing(); } private void assertProviderIsActive() { assertTrue("Component provider is not showing", runSwing(() -> tool.isVisible(provider))); // note: we can't call 'isActive()' due to focus issues in parallel testing //assertTrue("The test provider is not showing and focused", // runSwing(() -> tool.isActive(provider))); } private void assertProviderIsHidden_InNonBatchMode() { boolean isVisible = runSwing(() -> tool.isVisible(provider)); if (!SystemUtilities.isInTestingBatchMode()) { assertFalse("The test provider is showing, but should be hidden", isVisible); return; } if (isVisible) { Msg.error(this, "Provider should not be visible after pressing the 'close provider' key bindings"); } } private void assertNoToolbarAction() { assertNull("No toolbar action found for provider", getToolbarShowProviderAction()); } private void assertToolbarAction() { assertNotNull("No toolbar action found for provider", getToolbarShowProviderAction()); } private void assertProviderKeyStroke(KeyStroke expectedKs) { DockingActionIf action = getShowProviderAction(); KeyStroke actionKs = action.getKeyBinding(); assertEquals(expectedKs, actionKs); } private void assertOptionsKeyStroke(KeyStroke expectedKs) { ToolOptions options = getKeyBindingOptions(); // Option name: the action name with the 'Shared' owner String fullName = provider.getName() + " (Shared)"; ActionTrigger actionTrigger = runSwing(() -> options.getActionTrigger(fullName, null)); KeyStroke optionsKs = null; if (actionTrigger != null) { optionsKs = actionTrigger.getKeyStroke(); } assertEquals("Key stroke in options does not match expected key stroke", expectedKs, optionsKs); } private void assertWindowMenuActionHasIcon(Icon expected) { DockingActionIf action = getWindowMenuShowProviderAction(); Icon menuIcon = action.getMenuBarData().getMenuIcon(); assertEquals("Windows menu icons for provider does not match the value set on the provider", getDescription(expected), getDescription(menuIcon)); } private String getDescription(Icon icon) { if (icon instanceof GIcon) { return ((GIcon) icon).getUrl().toString(); } if (icon instanceof ImageIcon) { return ((ImageIcon) icon).getDescription(); } return icon.toString(); } private void assertToolbarActionHasIcon(Icon expected) { DockingActionIf action = getToolbarShowProviderAction(); assertNotNull("No toolbar action found; it should be there", action); ToolBarData tbData = action.getToolBarData(); assertEquals(expected, tbData.getIcon()); } private void assertWindowMenuActionHasKeyBinding(KeyStroke ks) { DockingActionIf action = getWindowMenuShowProviderAction(); assertEquals( "Windows menu key bindings for provider does not match the value set on the provider", ks, action.getKeyBinding()); } private void assertCannotShowKeyBindingDialog_FromWindowsMenu() { // simulate the user mousing over the 'Window' menu's action DockingActionIf windowMenuAction = getWindowMenuShowProviderAction(); DockingWindowManager.setMouseOverAction(windowMenuAction); performLaunchKeyStrokeDialogAction(); DialogComponentProvider warningDialog = waitForDialogComponent("Unable to Set Key Binding"); close(warningDialog); } private void setKeyBindingViaF4Dialog_FromWindowsMenu(KeyStroke ks) { // simulate the user mousing over the 'Window' menu's action DockingActionIf windowMenuAction = getWindowMenuShowProviderAction(); DockingWindowManager.setMouseOverAction(windowMenuAction); performLaunchKeyStrokeDialogAction(); KeyEntryDialog dialog = waitForDialogComponent(KeyEntryDialog.class); runSwing(() -> dialog.setKeyStroke(ks)); pressButtonByText(dialog, "OK"); assertFalse("Invalid key stroke: " + ks, runSwing(() -> dialog.isVisible())); } private void setKeyBindingViaF4Dialog_FromCloseButton(KeyStroke ks) { // simulate the user mousing over the toolbar button DockingActionIf closeAction = getAction(tool, provider.getOwner(), "Close Window"); assertNotNull("Provider action not installed in toolbar", closeAction); DockingWindowManager.setMouseOverAction(closeAction); performLaunchKeyStrokeDialogAction(); KeyEntryDialog dialog = waitForDialogComponent(KeyEntryDialog.class); runSwing(() -> dialog.setKeyStroke(ks)); pressButtonByText(dialog, "OK"); waitForSwing(); assertFalse("Invalid key stroke: " + ks, runSwing(() -> dialog.isVisible())); } private void applyBindingToDialog_FromWindowsMenu(KeyStroke ks) { DockingActionIf windowMenuAction = getWindowMenuShowProviderAction(); DockingWindowManager.setMouseOverAction(windowMenuAction); performLaunchKeyStrokeDialogAction(); KeyEntryDialog dialog = waitForDialogComponent(KeyEntryDialog.class); runSwing(() -> dialog.setKeyStroke(ks)); } private void assertCollisionsWithKeyStroke() { KeyEntryDialog dialog = waitForDialogComponent(KeyEntryDialog.class); JTextPane collisionPane = (JTextPane) getInstanceField("collisionPane", dialog); String collisionText = runSwing(() -> collisionPane.getText()); assertTrue(collisionText.contains("Actions mapped to")); } private void assertMenuItemHasKeyStroke(KeyStroke expected) { DockingActionIf action = getWindowMenuShowProviderAction(); assertEquals( "Windows menu key binding for provider does not match the value of the provider", expected, action.getKeyBinding()); } private void setKeyBindingViaF4Dialog_FromToolToolbar(KeyStroke ks) { // simulate the user mousing over the toolbar button DockingActionIf toolbarAction = getToolbarShowProviderAction(); assertNotNull("Provider action not installed in toolbar", toolbarAction); DockingWindowManager.setMouseOverAction(toolbarAction); performLaunchKeyStrokeDialogAction(); KeyEntryDialog dialog = waitForDialogComponent(KeyEntryDialog.class); runSwing(() -> dialog.setKeyStroke(ks)); pressButtonByText(dialog, "OK"); assertFalse("Invalid key stroke: " + ks, runSwing(() -> dialog.isVisible())); } private void performLaunchKeyStrokeDialogAction() { ToolActions toolActions = (ToolActions) tool.getToolActions(); Action action = toolActions.getAction(KeyStroke.getKeyStroke("F4")); assertNotNull(action); runSwing(() -> { SystemKeyBindingAction sysAction = (SystemKeyBindingAction) action; ExecutableAction executableAction = sysAction.getExecutableAction(null); executableAction.execute(); }, false); } private ToolOptions getKeyBindingOptions() { return tool.getOptions(DockingToolConstants.KEY_BINDINGS); } private class TestActionsComponentProvider extends ComponentProvider { private JComponent component = new JTextField("Hey!"); TestActionsComponentProvider(Tool tool) { super(tool, PROVIDER_NAME, "Fooberry Plugin"); } @Override public JComponent getComponent() { return component; } } private class HasDefaultKeyBindingComponentProvider extends ComponentProvider { private JComponent component = new JTextField("Hey!"); HasDefaultKeyBindingComponentProvider(Tool tool) { super(tool, HasDefaultKeyBindingComponentProvider.class.getSimpleName(), "Fooberry Plugin"); setKeyBinding(new KeyBindingData(CONTROL_T)); } @Override public JComponent getComponent() { return component; } } }