diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/functionwindow/FunctionWindowPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/functionwindow/FunctionWindowPlugin.java index da966862f6..dfd5217867 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/functionwindow/FunctionWindowPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/functionwindow/FunctionWindowPlugin.java @@ -15,12 +15,9 @@ */ package ghidra.app.plugin.core.functionwindow; -import javax.swing.KeyStroke; - import docking.ComponentProvider; import docking.ComponentProviderActivationListener; import docking.action.DockingAction; -import docking.action.KeyBindingData; import ghidra.app.CorePluginPackage; import ghidra.app.events.ProgramClosedPluginEvent; import ghidra.app.plugin.PluginCategoryNames; @@ -29,8 +26,6 @@ import ghidra.app.plugin.core.functioncompare.FunctionComparisonProvider; import ghidra.app.plugin.core.functioncompare.actions.CompareFunctionsFromFunctionTableAction; import ghidra.app.services.FunctionComparisonService; import ghidra.framework.model.*; -import ghidra.framework.options.OptionsChangeListener; -import ghidra.framework.options.ToolOptions; import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.util.PluginStatus; @@ -55,7 +50,7 @@ import ghidra.util.task.SwingUpdateManager; ) //@formatter:on public class FunctionWindowPlugin extends ProgramPlugin implements DomainObjectListener, - OptionsChangeListener, ComponentProviderActivationListener { + ComponentProviderActivationListener { private DockingAction selectAction; private DockingAction compareFunctionsAction; @@ -66,12 +61,7 @@ public class FunctionWindowPlugin extends ProgramPlugin implements DomainObjectL public FunctionWindowPlugin(PluginTool tool) { super(tool, true, false); - swingMgr = new SwingUpdateManager(1000, new Runnable() { - @Override - public void run() { - provider.reload(); - } - }); + swingMgr = new SwingUpdateManager(1000, () -> provider.reload()); } @Override @@ -221,20 +211,6 @@ public class FunctionWindowPlugin extends ProgramPlugin implements DomainObjectL tool.addLocalAction(provider, compareFunctionsAction); } - @Override - public void optionsChanged(ToolOptions options, String optionName, Object oldValue, - Object newValue) { - - if (optionName.startsWith(selectAction.getName())) { - KeyStroke keyStroke = (KeyStroke) newValue; - selectAction.setUnvalidatedKeyBindingData(new KeyBindingData(keyStroke)); - } - if (optionName.startsWith(compareFunctionsAction.getName())) { - KeyStroke keyStroke = (KeyStroke) newValue; - compareFunctionsAction.setUnvalidatedKeyBindingData(new KeyBindingData(keyStroke)); - } - } - void showFunctions() { provider.showFunctions(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/MakeProgramSelectionAction.java b/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/MakeProgramSelectionAction.java index 94e1b34493..dd67254c69 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/MakeProgramSelectionAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/MakeProgramSelectionAction.java @@ -51,6 +51,8 @@ public class MakeProgramSelectionAction extends DockingAction { */ public MakeProgramSelectionAction(String owner, GhidraTable table) { super("Make Selection", owner, KeyBindingType.SHARED); + this.table = table; + init(); } /** @@ -65,6 +67,10 @@ public class MakeProgramSelectionAction extends DockingAction { this.plugin = plugin; this.table = table; + init(); + } + + private void init() { setPopupMenuData( new MenuData(new String[] { "Make Selection" }, Icons.MAKE_SELECTION_ICON)); setToolBarData(new ToolBarData(Icons.MAKE_SELECTION_ICON)); diff --git a/Ghidra/Features/Base/src/test.slow/java/docking/ComponentProviderActionsTest.java b/Ghidra/Features/Base/src/test.slow/java/docking/ComponentProviderActionsTest.java index b42e10852e..e41e20780f 100644 --- a/Ghidra/Features/Base/src/test.slow/java/docking/ComponentProviderActionsTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/docking/ComponentProviderActionsTest.java @@ -169,7 +169,7 @@ public class ComponentProviderActionsTest extends AbstractGhidraHeadedIntegratio setKeyBindingViaF4Dialog_FromWindowsMenu(newKs); hideProvider(); - pressKey(CONTROL_T); + triggerKey(tool.getToolFrame(), CONTROL_T); assertProviderIsActive(); } @@ -337,7 +337,7 @@ public class ComponentProviderActionsTest extends AbstractGhidraHeadedIntegratio // 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. - pressKey(controlEsc); + triggerKey(tool.getToolFrame(), controlEsc); assertProviderIsHidden_InNonBatchMode(); } @@ -398,14 +398,6 @@ public class ComponentProviderActionsTest extends AbstractGhidraHeadedIntegratio }); } - private void pressKey(KeyStroke ks) { - int modifiers = ks.getModifiers(); - char keyChar = ks.getKeyChar(); - int keyCode = ks.getKeyCode(); - JFrame toolFrame = tool.getToolFrame(); - triggerKey(toolFrame, modifiers, keyCode, keyChar); - } - private DockingActionIf getShowProviderAction() { DockingActionIf showProviderAction = diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/tablechooser/TableChooserDialogTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/tablechooser/TableChooserDialogTest.java index d8e1d4ad02..cdf73b6c38 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/tablechooser/TableChooserDialogTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/tablechooser/TableChooserDialogTest.java @@ -15,31 +15,33 @@ */ package ghidra.app.tablechooser; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import javax.swing.*; +import org.junit.*; + +import docking.*; +import docking.action.*; +import docking.actions.KeyEntryDialog; +import docking.actions.ToolActions; +import docking.tool.util.DockingToolConstants; import ghidra.app.nav.Navigatable; +import ghidra.framework.options.ToolOptions; import ghidra.framework.plugintool.DummyPluginTool; import ghidra.program.model.address.Address; import ghidra.program.model.address.TestAddress; import ghidra.program.model.listing.Program; import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.ToyProgramBuilder; +import resources.Icons; import util.CollectionUtils; public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest { @@ -48,8 +50,9 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest private static final TestExecutorDecision DEFAULT_DECISION = r -> true; private DummyPluginTool tool; - private TableChooserDialog dialog; private SpyTableChooserExecutor executor; + private TableChooserDialog dialog; + private TestAction testAction; /** Interface for tests to signal what is expected of the executor */ private TestExecutorDecision testDecision = DEFAULT_DECISION; @@ -64,7 +67,6 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest public void tearDown() { runSwing(() -> { tool.close(); - //dialog.close(); }); } @@ -76,6 +78,10 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest Program program = new ToyProgramBuilder("Test", true).getProgram(); Navigatable navigatable = null; dialog = new TableChooserDialog(tool, executor, program, "Title", navigatable); + + testAction = new TestAction(); + dialog.addAction(testAction); + dialog.show(); loadData(); } @@ -250,10 +256,104 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest assertOnlyExecutedOnce(selected2); } + @Test + public void testActionToolBarButtonIconUpdate() { + + Icon icon = testAction.getToolBarData().getIcon(); + JButton button = getToolBarButton(icon); + assertNotNull("Could not find button for icon: " + icon, button); + + Icon newIcon = Icons.LEFT_ICON; + runSwing(() -> testAction.setToolBarData(new ToolBarData(newIcon))); + button = getToolBarButton(newIcon); + assertNotNull("Could not find button for icon: " + icon, button); + } + + @Test + public void testActionKeyBinding() { + KeyStroke ks = testAction.getKeyBinding(); + triggerKey(dialog.getComponent(), ks); + assertTrue(testAction.wasInvoked()); + } + + @Test + public void testActionKeyBinding_ChangeKeyBinding_FromOptions() { + KeyStroke newKs = KeyStroke.getKeyStroke('A', 0, false); + setOptionsKeyStroke(testAction, newKs); + triggerKey(dialog.getComponent(), newKs); + assertTrue(testAction.wasInvoked()); + } + + @Test + public void testActionKeyBinding_ChangeKeyBinding_FromKeyBindingDialog() { + KeyStroke newKs = KeyStroke.getKeyStroke('A', 0, false); + setKeyBindingViaF4Dialog(testAction, newKs); + triggerKey(dialog.getComponent(), newKs); + assertTrue(testAction.wasInvoked()); + } + + @Test + public void testSetKeyBindingUpdatesToolBarButtonTooltip() { + + JButton button = getToolBarButton(testAction); + String toolTip = button.getToolTipText(); + assertTrue(toolTip.contains("(Z)")); + + KeyStroke newKs = KeyStroke.getKeyStroke('A', 0, false); + setOptionsKeyStroke(testAction, newKs); + + String newToolTip = button.getToolTipText(); + assertTrue(newToolTip.contains("(A)")); + } + //================================================================================================== // Private Methods //================================================================================================== + private void setKeyBindingViaF4Dialog(DockingAction action, KeyStroke ks) { + + // simulate the user mousing over the toolbar button + assertNotNull("Provider action not installed in toolbar", action); + DockingWindowManager.setMouseOverAction(action); + + performLaunchKeyStrokeDialogAction(); + KeyEntryDialog keyDialog = waitForDialogComponent(KeyEntryDialog.class); + + runSwing(() -> keyDialog.setKeyStroke(ks)); + + pressButtonByText(keyDialog, "OK"); + + assertFalse("Invalid key stroke: " + ks, runSwing(() -> keyDialog.isVisible())); + } + + private void performLaunchKeyStrokeDialogAction() { + ToolActions toolActions = (ToolActions) ((AbstractDockingTool) tool).getToolActions(); + Action action = toolActions.getAction(KeyStroke.getKeyStroke("F4")); + assertNotNull(action); + runSwing(() -> action.actionPerformed(new ActionEvent(this, 0, "")), false); + } + + private void setOptionsKeyStroke(DockingAction action, KeyStroke newKs) { + + ToolOptions keyOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS); + + String name = action.getName() + " (" + action.getOwner() + ")"; + runSwing(() -> keyOptions.setKeyStroke(name, newKs)); + waitForSwing(); + + KeyStroke actual = action.getKeyBinding(); + assertEquals("Key binding was not updated after changing options", newKs, actual); + } + + private JButton getToolBarButton(TestAction action) { + return getToolBarButton(action.getToolBarData().getIcon()); + } + + private JButton getToolBarButton(Icon icon) { + JButton button = findButtonByIcon(dialog.getComponent(), icon); + return button; + } + private void assertRowCount(int expected) { int actual = getRowCount(); assertEquals("Table model row count is not as expected", expected, actual); @@ -400,4 +500,29 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest return getAddress().toString(); } } + + private class TestAction extends DockingAction { + + private int invoked; + + TestAction() { + super("Test Action", "Test Owner"); + + KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_Z, 0, false); + setKeyBindingData(new KeyBindingData(ks)); + setToolBarData(new ToolBarData(Icons.ERROR_ICON)); + } + + @Override + public void actionPerformed(ActionContext context) { + invoked++; + } + + boolean wasInvoked() { + if (invoked > 1) { + fail("Action invoked more than once"); + } + return invoked == 1; + } + } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/plugintool/dialog/KeyBindingUtilsTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/plugintool/dialog/KeyBindingUtilsTest.java index d872ad9828..8384bb95fd 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/plugintool/dialog/KeyBindingUtilsTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/plugintool/dialog/KeyBindingUtilsTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.*; import java.awt.Rectangle; import java.awt.Window; +import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.io.*; import java.util.*; @@ -152,6 +153,33 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest { debug.close(); } + @Test + public void testParseKeyStroke() { + + KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_V, 0); + String parsed = KeyBindingUtils.parseKeyStroke(ks); + assertEquals("V", parsed); + + ks = KeyStroke.getKeyStroke('v'); + parsed = KeyBindingUtils.parseKeyStroke(ks); + assertEquals("v", parsed); + + int modifiers = InputEvent.SHIFT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK; + ks = KeyStroke.getKeyStroke(KeyEvent.VK_V, modifiers); + parsed = KeyBindingUtils.parseKeyStroke(ks); + assertEquals("Ctrl-Shift-V", parsed); + + ks = KeyStroke.getKeyStroke(KeyEvent.VK_V, modifiers, true); + parsed = KeyBindingUtils.parseKeyStroke(ks); + assertEquals("Ctrl-Shift-V", parsed); + + JButton b = new JButton(); + KeyEvent event = new KeyEvent(b, KeyEvent.KEY_PRESSED, 1, modifiers, KeyEvent.VK_V, 'v'); + ks = KeyStroke.getKeyStrokeForEvent(event); + parsed = KeyBindingUtils.parseKeyStroke(ks); + assertEquals("Ctrl-Shift-V", parsed); + } + /* * Test method for 'ghidra.framework.plugintool.dialog.KeyBindingUtils.importKeyBindings(PluginTool)' */ @@ -364,7 +392,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest { private void reopenTool(PluginTool tool2) { runSwing(() -> { ToolServices services = tool.getProject().getToolServices(); - tool = (PluginTool) services.launchTool(tool.getName(), null); + tool = services.launchTool(tool.getName(), null); }); assertNotNull(tool); } diff --git a/Ghidra/Features/Decompiler/ghidra_scripts/CompareFunctionSizesScript.java b/Ghidra/Features/Decompiler/ghidra_scripts/CompareFunctionSizesScript.java index 3ef0029662..aa4a464b9c 100644 --- a/Ghidra/Features/Decompiler/ghidra_scripts/CompareFunctionSizesScript.java +++ b/Ghidra/Features/Decompiler/ghidra_scripts/CompareFunctionSizesScript.java @@ -23,7 +23,8 @@ // @category Analysis -import java.util.*; +import java.util.Iterator; +import java.util.function.Consumer; import org.apache.commons.collections4.IteratorUtils; @@ -33,6 +34,8 @@ import ghidra.app.script.GhidraScript; import ghidra.app.tablechooser.*; import ghidra.program.model.address.Address; import ghidra.program.model.listing.*; +import ghidra.program.model.pcode.HighFunction; +import ghidra.program.model.pcode.PcodeOpAST; import ghidra.util.task.TaskMonitor; public class CompareFunctionSizesScript extends GhidraScript { @@ -41,51 +44,44 @@ public class CompareFunctionSizesScript extends GhidraScript { protected void run() throws Exception { if (isRunningHeadless()) { - printf("This script cannot be run headlessly.\n"); + println("This script cannot be run headlessly"); return; } - DecompilerCallback callback = new DecompilerCallback( + TableChooserDialog tableDialog = + createTableChooserDialog(currentProgram.getName() + " function sizes", null); + configureTableColumns(tableDialog); + tableDialog.show(); + + DecompilerCallback callback = new DecompilerCallback<>( currentProgram, new CompareFunctionSizesScriptConfigurer(currentProgram)) { @Override public FuncBodyData process(DecompileResults results, TaskMonitor tMonitor) throws Exception { - InstructionIterator instIter = currentProgram.getListing().getInstructions( - results.getFunction().getBody(), true); - int numInstructions = IteratorUtils.size(instIter); - //indicate failure of decompilation by having 0 high pcode ops + + Listing listing = currentProgram.getListing(); + Function function = results.getFunction(); + InstructionIterator it = listing.getInstructions(function.getBody(), true); + int numInstructions = IteratorUtils.size(it); + + // indicate failure of decompilation by having 0 high pcode ops int numHighOps = 0; - if (results.getHighFunction() != null && - results.getHighFunction().getPcodeOps() != null) { - numHighOps = IteratorUtils.size(results.getHighFunction().getPcodeOps()); + HighFunction highFunction = results.getHighFunction(); + if (highFunction != null) { + Iterator ops = highFunction.getPcodeOps(); + if (ops != null) { + numHighOps = IteratorUtils.size(ops); + } } - return new FuncBodyData(results.getFunction(), numInstructions, numHighOps); + return new FuncBodyData(function, numInstructions, numHighOps); } }; - Set funcsToDecompile = new HashSet<>(); - FunctionIterator fIter = currentProgram.getFunctionManager().getFunctionsNoStubs(true); - fIter.forEach(e -> funcsToDecompile.add(e)); - - if (funcsToDecompile.isEmpty()) { - popup("No functions to decompile!"); - return; - } - - List funcBodyData = ParallelDecompiler.decompileFunctions(callback, - currentProgram, funcsToDecompile, monitor); - - monitor.checkCanceled(); - - TableChooserDialog tableDialog = - createTableChooserDialog(currentProgram.getName() + " function sizes", null); - configureTableColumns(tableDialog); - - tableDialog.show(); - for (FuncBodyData bodyData : funcBodyData) { - tableDialog.add(bodyData); - } + Consumer consumer = data -> tableDialog.add(data); + FunctionIterator it = currentProgram.getFunctionManager().getFunctionsNoStubs(true); + ParallelDecompiler.decompileFunctions(callback, currentProgram, it, consumer, monitor); + callback.dispose(); } class CompareFunctionSizesScriptConfigurer implements DecompileConfigurer { @@ -210,7 +206,7 @@ public class CompareFunctionSizesScript extends GhidraScript { } }; - ColumnDisplay highOpsColumn = new AbstractComparableColumnDisplay() { + ColumnDisplay highOpsColumn = new AbstractComparableColumnDisplay<>() { @Override public Integer getColumnValue(AddressableRowObject rowObject) { @@ -223,7 +219,7 @@ public class CompareFunctionSizesScript extends GhidraScript { } }; - ColumnDisplay instructionColumn = new AbstractComparableColumnDisplay() { + ColumnDisplay instructionColumn = new AbstractComparableColumnDisplay<>() { @Override public Integer getColumnValue(AddressableRowObject rowObject) { @@ -236,7 +232,7 @@ public class CompareFunctionSizesScript extends GhidraScript { } }; - ColumnDisplay ratioColumn = new AbstractComparableColumnDisplay() { + ColumnDisplay ratioColumn = new AbstractComparableColumnDisplay<>() { @Override public Double getColumnValue(AddressableRowObject rowObject) { diff --git a/Ghidra/Features/Decompiler/ghidra_scripts/FindPotentialDecompilerProblems.java b/Ghidra/Features/Decompiler/ghidra_scripts/FindPotentialDecompilerProblems.java index ee704f35d1..b6af92a3cd 100644 --- a/Ghidra/Features/Decompiler/ghidra_scripts/FindPotentialDecompilerProblems.java +++ b/Ghidra/Features/Decompiler/ghidra_scripts/FindPotentialDecompilerProblems.java @@ -76,7 +76,7 @@ public class FindPotentialDecompilerProblems extends GhidraScript { return; } - ParallelDecompiler.decompileFunctions(callback, currentProgram, funcsToDecompile, monitor); + ParallelDecompiler.decompileFunctions(callback, funcsToDecompile, monitor); monitor.checkCanceled(); tableDialog.setMessage("Finished"); } diff --git a/Ghidra/Features/Decompiler/ghidra_scripts/FixSwitchStatementsWithDecompiler.java b/Ghidra/Features/Decompiler/ghidra_scripts/FixSwitchStatementsWithDecompiler.java index cd96eff843..4c16511c4d 100644 --- a/Ghidra/Features/Decompiler/ghidra_scripts/FixSwitchStatementsWithDecompiler.java +++ b/Ghidra/Features/Decompiler/ghidra_scripts/FixSwitchStatementsWithDecompiler.java @@ -60,7 +60,7 @@ public class FixSwitchStatementsWithDecompiler extends GhidraScript { Set functions = instructionsByFunction.keySet(); try { - ParallelDecompiler.decompileFunctions(callback, currentProgram, functions, monitor); + ParallelDecompiler.decompileFunctions(callback, functions, monitor); } finally { callback.dispose(); diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/parallel/ParallelDecompiler.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/parallel/ParallelDecompiler.java index ac8a5dcbe4..c296953336 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/parallel/ParallelDecompiler.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/parallel/ParallelDecompiler.java @@ -16,6 +16,7 @@ package ghidra.app.decompiler.parallel; import java.util.*; +import java.util.function.Consumer; import generic.concurrent.QCallback; import generic.concurrent.QResult; @@ -46,7 +47,7 @@ public class ParallelDecompiler { Listing listing = program.getListing(); FunctionIterator iterator = listing.getFunctions(addresses, true); - List results = decompileFunctions(callback, program, iterator, functionCount, monitor); + List results = doDecompileFunctions(callback, iterator, functionCount, monitor); return results; } @@ -54,23 +55,54 @@ public class ParallelDecompiler { * Decompile the given functions using multiple decompilers * * @param callback the callback to be called for each that is processed - * @param program the program * @param functions the functions to decompile * @param monitor the task monitor * @return the list of client results * @throws InterruptedException if interrupted * @throws Exception if any other exception occurs */ - public static List decompileFunctions(QCallback callback, Program program, - Collection functions, TaskMonitor monitor) + public static List decompileFunctions(QCallback callback, + Collection functions, + TaskMonitor monitor) throws InterruptedException, Exception { List results = - decompileFunctions(callback, program, functions.iterator(), functions.size(), monitor); + doDecompileFunctions(callback, functions.iterator(), functions.size(), + monitor); return results; } - private static List decompileFunctions(QCallback callback, Program program, + /** + * Decompile the given functions using multiple decompilers. + * + *

Results will be passed to the given consumer as they are produced. Calling this + * method allows you to handle results as they are discovered. + * + *

This method will wait for all processing before returning. + * + * @param callback the callback to be called for each that is processed + * @param program the program + * @param functions the functions to decompile + * @param resultsConsumer the consumer to which results will be passed + * @param monitor the task monitor + * @throws InterruptedException if interrupted + * @throws Exception if any other exception occurs + */ + public static void decompileFunctions(QCallback callback, + Program program, + Iterator functions, Consumer resultsConsumer, + TaskMonitor monitor) + throws InterruptedException, Exception { + + int max = program.getFunctionManager().getFunctionCount(); + DecompilerConcurrentQ queue = + new DecompilerConcurrentQ<>(callback, THREAD_POOL_NAME, monitor); + monitor.initialize(max); + queue.process(functions, resultsConsumer); + queue.waitUntilDone(); + } + + private static List doDecompileFunctions(QCallback callback, Iterator functions, int count, TaskMonitor monitor) throws InterruptedException, Exception { diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/analysis/DecompilerSwitchAnalyzer.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/analysis/DecompilerSwitchAnalyzer.java index 2c1275316c..8f7e393369 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/analysis/DecompilerSwitchAnalyzer.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/analysis/DecompilerSwitchAnalyzer.java @@ -162,7 +162,7 @@ public class DecompilerSwitchAnalyzer extends AbstractAnalyzer { callback.setTimeout(decompilerTimeoutSecondsOption); try { - ParallelDecompiler.decompileFunctions(callback, program, functions, monitor); + ParallelDecompiler.decompileFunctions(callback, functions, monitor); } finally { callback.dispose(); diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/analysis/ObjectiveC2_DecompilerMessageAnalyzer.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/analysis/ObjectiveC2_DecompilerMessageAnalyzer.java index 0f3b82b2c5..c04d6eef58 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/analysis/ObjectiveC2_DecompilerMessageAnalyzer.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/analysis/ObjectiveC2_DecompilerMessageAnalyzer.java @@ -108,7 +108,7 @@ public class ObjectiveC2_DecompilerMessageAnalyzer extends AbstractAnalyzer { }; try { - ParallelDecompiler.decompileFunctions(callback, program, functions, monitor); + ParallelDecompiler.decompileFunctions(callback, functions, monitor); } finally { callback.dispose(); diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompiler/validator/DecompilerValidator.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompiler/validator/DecompilerValidator.java index 531351a39a..cb0f62640d 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompiler/validator/DecompilerValidator.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompiler/validator/DecompilerValidator.java @@ -62,7 +62,7 @@ public class DecompilerValidator extends PostAnalysisValidator { try { List results = - ParallelDecompiler.decompileFunctions(callback, program, functions, monitor); + ParallelDecompiler.decompileFunctions(callback, functions, monitor); return processResults(results); } catch (Exception e) { diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/util/DecompilerConcurrentQ.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/util/DecompilerConcurrentQ.java index c6fba0e3c6..ab9450e7c0 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/util/DecompilerConcurrentQ.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/util/DecompilerConcurrentQ.java @@ -15,26 +15,39 @@ */ package ghidra.app.util; -import java.util.Collection; -import java.util.Iterator; +import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import generic.concurrent.*; import ghidra.app.plugin.core.analysis.AutoAnalysisManager; import ghidra.util.Msg; import ghidra.util.task.TaskMonitor; +import utility.function.Dummy; /** * A class to perform some of the boilerplate setup of the {@link ConcurrentQ} that is shared * amongst clients that perform decompilation in parallel. + * + *

This class can be used in a blocking or non-blocking fashion. + * + *

    + *
  • For blocking usage, call + * one of the {@code add} methods to put items in the queue and then call + * {@link #waitForResults()}.
  • + *
  • For non-blocking usage, simply call + * {@link #process(Iterator, Consumer)}, passing the consumer of the results.
  • + * + *

    * * @param The input data needed by the supplied {@link QCallback} - * @param The result data (can be the same as {@link I} if there is no result) returned + * @param The result data (can be the same as {@code I} if there is no result) returned * by the {@link QCallback#process(Object, TaskMonitor)} method. */ public class DecompilerConcurrentQ { private ConcurrentQ queue; + private Consumer resultConsumer = Dummy.consumer(); public DecompilerConcurrentQ(QCallback callback, TaskMonitor monitor) { this(callback, AutoAnalysisManager.getSharedAnalsysThreadPool(), monitor); @@ -51,6 +64,7 @@ public class DecompilerConcurrentQ { .setCollectResults(true) .setThreadPool(pool) .setMonitor(monitor) + .setListener(new InternalResultListener()) .build(callback); // @formatter:on } @@ -67,6 +81,18 @@ public class DecompilerConcurrentQ { queue.add(i); } + /** + * Adds all items to the queue for processing. The results will be passed to the given consumer + * as they are produced. + * + * @param functions the functions to process + * @param consumer the results consumer + */ + public void process(Iterator functions, Consumer consumer) { + this.resultConsumer = Objects.requireNonNull(consumer); + addAll(functions); + } + /** * Waits for all results to be delivered. The client is responsible for processing the * results and handling any exceptions that may have occurred. @@ -75,15 +101,12 @@ public class DecompilerConcurrentQ { * @throws InterruptedException if interrupted while waiting */ public Collection> waitForResults() throws InterruptedException { - Collection> results = null; try { - results = queue.waitForResults(); + return queue.waitForResults(); } finally { queue.dispose(); } - - return results; } /** @@ -95,7 +118,12 @@ public class DecompilerConcurrentQ { * @throws Exception any exception that is encountered while processing items. */ public void waitUntilDone() throws InterruptedException, Exception { - queue.waitUntilDone(); + try { + queue.waitUntilDone(); + } + finally { + queue.dispose(); + } } public void dispose() { @@ -125,4 +153,23 @@ public class DecompilerConcurrentQ { "Unable to shutdown all tasks in " + timeoutSeconds + " " + TimeUnit.SECONDS); } } + + private class InternalResultListener implements QItemListener { + @Override + public void itemProcessed(QResult result) { + try { + R r = result.getResult(); + if (r != null) { + resultConsumer.accept(r); + } + } + catch (Throwable t) { + // This code is an asynchronous callback. Handle the exception the same way as + // the waitXyz() method do, which is to shutdown the queue. + Msg.error(this, "Unexpected exception getting Decompiler result", t); + queue.dispose(); + } + + } + } } diff --git a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/DecompilerDataTypeReferenceFinder.java b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/DecompilerDataTypeReferenceFinder.java index d54cf355c0..f597491948 100644 --- a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/DecompilerDataTypeReferenceFinder.java +++ b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/DecompilerDataTypeReferenceFinder.java @@ -59,7 +59,7 @@ public class DecompilerDataTypeReferenceFinder implements DataTypeReferenceFinde Set functions = filterFunctions(program, dataType, monitor); try { - ParallelDecompiler.decompileFunctions(qCallback, program, functions, monitor); + ParallelDecompiler.decompileFunctions(qCallback, functions, monitor); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // reset the flag @@ -83,7 +83,7 @@ public class DecompilerDataTypeReferenceFinder implements DataTypeReferenceFinde Set functions = filterFunctions(program, dataType, monitor); try { - ParallelDecompiler.decompileFunctions(qCallback, program, functions, monitor); + ParallelDecompiler.decompileFunctions(qCallback, functions, monitor); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // reset the flag diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java index 211749d103..97cf1a424d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java @@ -26,9 +26,7 @@ import org.apache.commons.lang3.StringUtils; import org.jdesktop.animation.timing.Animator; import org.jdesktop.animation.timing.TimingTargetAdapter; -import docking.action.ActionContextProvider; -import docking.action.DockingActionIf; -import docking.actions.ActionAdapter; +import docking.action.*; import docking.actions.KeyBindingUtils; import docking.event.mouse.GMouseListenerAdapter; import docking.help.HelpService; @@ -627,7 +625,7 @@ public class DialogComponentProvider public void setStatusText(String message, MessageType type, boolean alert) { String text = StringUtils.isBlank(message) ? " " : message; - SystemUtilities.runIfSwingOrPostSwingLater(() -> doSetStatusText(text, type, alert)); + Swing.runIfSwingOrRunLater(() -> doSetStatusText(text, type, alert)); } private void doSetStatusText(String text, MessageType type, boolean alert) { @@ -659,7 +657,7 @@ public class DialogComponentProvider */ protected void alertMessage(Callback alertFinishedCallback) { - SystemUtilities.runIfSwingOrPostSwingLater(() -> { + Swing.runIfSwingOrRunLater(() -> { doAlertMessage(alertFinishedCallback); }); } @@ -801,7 +799,7 @@ public class DialogComponentProvider */ @Override public void clearStatusText() { - SystemUtilities.runIfSwingOrPostSwingLater(() -> { + Swing.runIfSwingOrRunLater(() -> { statusLabel.setText(" "); updateStatusToolTip(); }); @@ -1171,18 +1169,6 @@ public class DialogComponentProvider } } - /** - * Add an action to this dialog. Only actions with icons are added to the toolbar. - * @param action popup menu action - */ - public void addAction(final DockingActionIf action) { - dialogActions.add(action); - addToolbarAction(action); - popupManager.addAction(action); - - registerActionKeyBinding(action); - } - public Set getActions() { return new HashSet<>(dialogActions); } @@ -1203,36 +1189,22 @@ public class DialogComponentProvider actionMap.put(action, button); } - private void registerActionKeyBinding(DockingActionIf dockingAction) { - String name = dockingAction.getName(); - KeyStroke stroke = dockingAction.getKeyBinding(); - if (stroke == null) { - return; - } - ActionAdapter actionAdapter = new ActionAdapter(dockingAction, this); - Object binding = null; // old binding for keyStroke; + /** + * Add an action to this dialog. Only actions with icons are added to the toolbar. + * Note, if you add an action to this dialog, do not also add the action to + * the tool, as this dialog will do that for you. + * @param action the action + */ + public void addAction(final DockingActionIf action) { + dialogActions.add(action); + addToolbarAction(action); + popupManager.addAction(action); - InputMap imap = rootPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); - if (imap != null) { - binding = imap.get(stroke); - imap.put(stroke, name); - } - - ActionMap amap = rootPanel.getActionMap(); - if (amap != null) { - if (binding != null) { - Action action = amap.get(binding); - if (action != null) { - if (action instanceof DockingActionIf) { - throw new AssertException( - "Attempted to register more than one acton with the same keybinding to this dialog! " + - stroke); - } - actionAdapter.setDefaultAction(action); - } - } - amap.put(name, actionAdapter); - } + // add the action to the tool in order get key event management (key bindings + // options and key event processing) + DockingWindowManager dwm = DockingWindowManager.getActiveInstance(); + Tool tool = dwm.getTool(); + tool.addAction(new DialogActionProxy(action)); } public void removeAction(DockingActionIf action) { @@ -1347,51 +1319,23 @@ public class DialogComponentProvider } -//================================================================================================== -// Testing... -//================================================================================================== + /** + * A placeholder action that we register with the tool in order to get key event management + */ + private class DialogActionProxy extends DockingActionProxy { - public static void main(String[] args) throws Exception { - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } - catch (Exception exc) { - Msg.error(DialogComponentProvider.class, "Error loading L&F: " + exc, exc); - } - JFrame frame = new JFrame("What"); - Joe joe = new Joe("My Test Dialog Component", true); - frame.setVisible(true); - JDialog d = DockingDialog.createDialog(frame, joe, null); - d.setVisible(true); - Thread.sleep(2000); - d = DockingDialog.createDialog(frame, joe, null); - d.setVisible(true); - frame.setVisible(false); - System.exit(0); - } - - static class Joe extends DialogComponentProvider { - Joe(String title, boolean modal) { - super(title, modal); - addOKButton(); - addCancelButton(); - addWorkPanel(new JButton("TEST")); + public DialogActionProxy(DockingActionIf dockingAction) { + super(dockingAction); } @Override - protected void okCallback() { - this.setStatusText("OK"); -// Task t = new BusyTask(); -// executeProgressTask(t, 500); - setOkEnabled(false); + public boolean isAddToPopup(ActionContext context) { + return false; } @Override - protected void cancelCallback() { - this.setStatusText("Cancel"); - clearScheduledTask(); - super.cancelCallback(); + public ToolBarData getToolBarData() { + return null; } - } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java b/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java index 7f78305998..c46e25eb02 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java @@ -1245,7 +1245,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder root.update(); // do this before rebuilding the menu, as new windows may be opened buildComponentMenu(); - SystemUtilities.runSwingLater(() -> updateFocus()); + Swing.runLater(() -> updateFocus()); } private void updateFocus() { @@ -1292,7 +1292,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder return; } - SystemUtilities.runSwingLater(() -> { + Swing.runLater(() -> { KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); Window activeWindow = kfm.getActiveWindow(); if (activeWindow == null) { @@ -1480,7 +1480,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder // Note: do this later, since, during this callback, component providers can do // things that break focus (e.g., launch a modal dialog). By doing this later, // it gives the java focus engine a chance to get in the correct state. - SystemUtilities.runSwingLater(() -> setFocusedComponent(placeholder)); + Swing.runLater(() -> setFocusedComponent(placeholder)); } private boolean ensureDockableComponentContainsFocusOwner(Component newFocusComponent, @@ -1515,7 +1515,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder // else use last focus component in window WindowNode node = root.getNodeForWindow(window); if (node == null) { - throw new AssertException("Cant find node for window!!"); + return null; } // NOTE: We only allow focus within a window on a component that belongs to within a @@ -1610,17 +1610,23 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder } private boolean isMyWindow(Window win) { - if (root == null) { + if (root == null || win == null) { return false; } - if (root.getMainWindow() == win) { + + Window rootFrame = root.getMainWindow(); + if (rootFrame == win) { return true; } - Iterator iter = root.getDetachedWindows().iterator(); - while (iter.hasNext()) { - if (iter.next().getWindow() == win) { - return true; - } + + WindowNode node = root.getNodeForWindow(win); + if (node != null) { + return true; + } + + // see if the given window is a child of the root node's frame + if (SwingUtilities.isDescendingFrom(win, rootFrame)) { + return true; } return false; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/EmptyBorderToggleButton.java b/Ghidra/Framework/Docking/src/main/java/docking/EmptyBorderToggleButton.java index 52bfd73ee4..d136a32a1b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/EmptyBorderToggleButton.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/EmptyBorderToggleButton.java @@ -15,7 +15,6 @@ */ package docking; - import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; @@ -94,7 +93,7 @@ public class EmptyBorderToggleButton extends EmptyBorderButton { return isSelected(); } - /** This method only functions if this class was created with an action */ + // This method only functions if this class was created with an action protected void doActionPerformed(ActionEvent e) { setSelected(!isSelected()); // toggle @@ -105,7 +104,7 @@ public class EmptyBorderToggleButton extends EmptyBorderButton { buttonAction.actionPerformed(e); } - private void doPropertyChange(PropertyChangeEvent e) { + protected void doPropertyChange(PropertyChangeEvent e) { String name = e.getPropertyName(); if (name.equals("enabled")) { setEnabled(((Boolean) e.getNewValue()).booleanValue()); @@ -165,14 +164,14 @@ public class EmptyBorderToggleButton extends EmptyBorderButton { return b; } - DefaultButtonModel model = (DefaultButtonModel) bm; - ButtonGroup group = model.getGroup(); + DefaultButtonModel buttonModel = (DefaultButtonModel) bm; + ButtonGroup group = buttonModel.getGroup(); if (group == null) { return b; } - group.setSelected(model, b); - boolean isSelected = group.isSelected(model); + group.setSelected(buttonModel, b); + boolean isSelected = group.isSelected(buttonModel); return isSelected; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java b/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java index 50011444ed..d6c38b4d70 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java @@ -127,7 +127,8 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher } // some known special cases that we don't wish to process - if (!isValidContextForKeyStroke(KeyStroke.getKeyStrokeForEvent(event))) { + KeyStroke ks = KeyStroke.getKeyStrokeForEvent(event); + if (!isValidContextForKeyStroke(ks)) { return false; } @@ -218,7 +219,13 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher private boolean isValidContextForKeyStroke(KeyStroke keyStroke) { Window activeWindow = focusProvider.getActiveWindow(); if (activeWindow instanceof DockingDialog) { - return false; // we don't want to process our key bindings when in DockingDialogs + + // This is legacy code, for which the reasons it exists cannot be recalled. We + // speculate that odd things can happen when keybindings are processed with model + // dialogs open. For now, do not let key bindings get processed for modal dialogs. + // This can be changed in the future if needed. + DockingDialog dialog = (DockingDialog) activeWindow; + return !dialog.isModal(); } return true; // default case; allow it through } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java b/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java index 9417957562..cf522bc31c 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java @@ -15,6 +15,7 @@ */ package docking.action; +import java.awt.Window; import java.awt.event.ActionEvent; import java.util.*; @@ -87,19 +88,16 @@ public class MultipleKeyAction extends DockingKeyBindingAction { */ @Override public boolean isEnabled() { - // always return true so we can report the status message - // when none of the actions is enabled... + // always return true so we can report the status message when all actions are disabled return true; } /** - * Enables or disables the action. This affects all uses - * of the action. Note that for popups, this affects whether or - * not the option is "grayed out", not whether the action is added + * Enables or disables the action. This affects all uses of the action. Note that for popups, + * this affects whether or not the option is "grayed out", not whether the action is added * to the popup. * - * @param newValue true to enable the action, false to - * disable it + * @param newValue true to enable the action, false to disable it * @see Action#setEnabled */ @Override @@ -111,17 +109,10 @@ public class MultipleKeyAction extends DockingKeyBindingAction { } } - /** - * Invoked when an action occurs. - */ @Override public void actionPerformed(final ActionEvent event) { // Build list of actions which are valid in current context - ComponentProvider localProvider = tool.getActiveComponentProvider(); - ActionContext localContext = getLocalContext(localProvider); - localContext.setSourceObject(event.getSource()); - - List list = getValidContextActions(localContext); + List list = getActionsForCurrentContext(event.getSource()); // If menu active, disable all key bindings if (ignoreActionWhileMenuShowing()) { @@ -237,10 +228,7 @@ public class MultipleKeyAction extends DockingKeyBindingAction { @Override public KeyBindingPrecedence getKeyBindingPrecedence() { - ComponentProvider localProvider = tool.getActiveComponentProvider(); - ActionContext localContext = getLocalContext(localProvider); - List validActions = getValidContextActions(localContext); - + List validActions = getActionsForCurrentContext(null); if (validActions.isEmpty()) { return null; // a signal that no actions are valid for the current context } @@ -254,6 +242,29 @@ public class MultipleKeyAction extends DockingKeyBindingAction { return action.getKeyBindingData().getKeyBindingPrecedence(); } + private List getActionsForCurrentContext(Object eventSource) { + + DockingWindowManager dwm = tool.getWindowManager(); + Window window = dwm.getActiveWindow(); + if (window instanceof DockingDialog) { + DockingDialog dockingDialog = (DockingDialog) window; + DialogComponentProvider provider = dockingDialog.getDialogComponent(); + if (provider == null) { + // this can happen if the dialog is closed during key event processing + return Collections.emptyList(); + } + ActionContext context = provider.getActionContext(null); + List validActions = getValidContextActions(context); + return validActions; + } + + ComponentProvider localProvider = dwm.getActiveComponentProvider(); + ActionContext localContext = getLocalContext(localProvider); + localContext.setSourceObject(eventSource); + List validActions = getValidContextActions(localContext); + return validActions; + } + public List getActions() { List list = new ArrayList<>(actions.size()); for (ActionData actionData : actions) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/ToolBarData.java b/Ghidra/Framework/Docking/src/main/java/docking/action/ToolBarData.java index 6debfcb46f..5f3c24e238 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/ToolBarData.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/ToolBarData.java @@ -18,6 +18,7 @@ package docking.action; import javax.swing.Icon; import docking.DockingUtils; +import generic.json.Json; import ghidra.util.SystemUtilities; public class ToolBarData { @@ -106,4 +107,9 @@ public class ToolBarData { ownerAction.firePropertyChanged(DockingActionIf.TOOLBAR_DATA_PROPERTY, oldData, this); } } + + @Override + public String toString() { + return Json.toString(this, "icon", "toolBarGroup", "toolBarSubGroup"); + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingUtils.java index bd5c4e5efa..652ec2900c 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingUtils.java @@ -27,14 +27,15 @@ import java.util.stream.Collectors; import javax.swing.*; import org.apache.commons.collections4.map.LazyMap; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jdom.*; import org.jdom.input.SAXBuilder; import org.jdom.output.XMLOutputter; -import docking.Tool; import docking.DockingUtils; +import docking.Tool; import docking.action.*; import docking.widgets.filechooser.GhidraFileChooser; import ghidra.framework.options.ToolOptions; @@ -666,6 +667,8 @@ public class KeyBindingUtils { * and we want it to look like: "Ctrl-M". *
    In Java 1.5.0, Ctrl-M is returned as "ctrl pressed M" * and we want it to look like: "Ctrl-M". + *
    In Java 11 we have seen toString() values get printed with repeated text, such + * as: "shift ctrl pressed SHIFT". We want to trim off the repeated modifiers. * * @param keyStroke the key stroke * @return the string value; the empty string if the key stroke is null @@ -685,19 +688,19 @@ public class KeyBindingUtils { // get the character used in the key stroke int firstIndex = keyString.lastIndexOf(' ') + 1; - int ctrlIndex = keyString.indexOf(CTRL, firstIndex); + int ctrlIndex = indexOf(keyString, CTRL, firstIndex); if (ctrlIndex >= 0) { firstIndex = ctrlIndex + CTRL.length(); } - int altIndex = keyString.indexOf(ALT, firstIndex); + int altIndex = indexOf(keyString, ALT, firstIndex); if (altIndex >= 0) { firstIndex = altIndex + ALT.length(); } - int shiftIndex = keyString.indexOf(SHIFT, firstIndex); + int shiftIndex = indexOf(keyString, SHIFT, firstIndex); if (shiftIndex >= 0) { firstIndex = shiftIndex + SHIFT.length(); } - int metaIndex = keyString.indexOf(META, firstIndex); + int metaIndex = indexOf(keyString, META, firstIndex); if (metaIndex >= 0) { firstIndex = metaIndex + META.length(); } @@ -714,18 +717,31 @@ public class KeyBindingUtils { StringBuilder buffy = new StringBuilder(); if (isShift(modifiers)) { buffy.insert(0, SHIFT + MODIFIER_SEPARATOR); + keyString = removeIgnoreCase(keyString, SHIFT); } if (isAlt(modifiers)) { buffy.insert(0, ALT + MODIFIER_SEPARATOR); + keyString = removeIgnoreCase(keyString, ALT); } if (isControl(modifiers)) { buffy.insert(0, CTRL + MODIFIER_SEPARATOR); + keyString = removeIgnoreCase(keyString, CONTROL); } if (isMeta(modifiers)) { buffy.insert(0, META + MODIFIER_SEPARATOR); + keyString = removeIgnoreCase(keyString, META); } buffy.append(keyString); - return buffy.toString(); + + String text = buffy.toString().trim(); + if (text.endsWith(MODIFIER_SEPARATOR)) { + text = text.substring(0, text.length() - 1); + } + return text; + } + + private static int indexOf(String source, String search, int offset) { + return StringUtils.indexOfIgnoreCase(source, search, offset); } // ignore the deprecated; remove when we are confident that all tool actions no longer use the diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/ToolActions.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/ToolActions.java index 6fd42d9a8f..773356ecd2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/actions/ToolActions.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/ToolActions.java @@ -18,6 +18,8 @@ package docking.actions; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.swing.Action; import javax.swing.KeyStroke; @@ -41,6 +43,9 @@ import util.CollectionUtils; */ public class ToolActions implements DockingToolActions, PropertyChangeListener { + // matches the full action name (e.g., "Action Name (Owner Name)" + private Pattern ACTION_NAME_PATTERN = Pattern.compile("(.+) \\((.+)\\)"); + private ActionToGuiHelper actionGuiHelper; /* @@ -57,6 +62,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener { private ToolOptions keyBindingOptions; private Tool dockingTool; private KeyBindingsManager keyBindingsManager; + private OptionsChangeListener optionChangeListener = (options, optionName, oldValue, + newValue) -> updateKeyBindingsFromOptions(options, optionName, (KeyStroke) newValue); /** * Construct an ActionManager @@ -69,6 +76,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener { this.actionGuiHelper = actionToGuiHelper; this.keyBindingsManager = new KeyBindingsManager(tool); this.keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS); + this.keyBindingOptions.addOptionsChangeListener(optionChangeListener); createReservedKeyBindings(); SharedActionRegistry.installSharedActions(tool, this); @@ -356,13 +364,33 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener { return actionsByNameByOwner.get(owner).get(name); } + private void updateKeyBindingsFromOptions(ToolOptions options, String optionName, + KeyStroke newKs) { + + // note: the 'shared actions' update themselves, so we only need to handle standard actions + + Matcher matcher = ACTION_NAME_PATTERN.matcher(optionName); + matcher.find(); + String name = matcher.group(1); + String owner = matcher.group(2); + + Set actions = actionsByNameByOwner.get(owner).get(name); + for (DockingActionIf action : actions) { + KeyStroke oldKs = action.getKeyBinding(); + if (Objects.equals(oldKs, newKs)) { + continue; // prevent bouncing + } + action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs)); + } + } + @Override public void propertyChange(PropertyChangeEvent evt) { if (!evt.getPropertyName().equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) { return; } - DockingAction action = (DockingAction) evt.getSource(); + DockingActionIf action = (DockingActionIf) evt.getSource(); if (!action.getKeyBindingType().isManaged()) { // this reads unusually, but we need to notify the tool to rebuild its 'Window' menu // in the case that this action is one of the tool's special actions @@ -376,13 +404,12 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener { newKeyStroke = newKeyBindingData.getKeyBinding(); } - Options opt = dockingTool.getOptions(DockingToolConstants.KEY_BINDINGS); - KeyStroke optKeyStroke = opt.getKeyStroke(action.getFullName(), null); + KeyStroke optKeyStroke = keyBindingOptions.getKeyStroke(action.getFullName(), null); if (newKeyStroke == null) { - opt.removeOption(action.getFullName()); + keyBindingOptions.removeOption(action.getFullName()); } else if (!newKeyStroke.equals(optKeyStroke)) { - opt.setKeyStroke(action.getFullName(), newKeyStroke); + keyBindingOptions.setKeyStroke(action.getFullName(), newKeyStroke); keyBindingsChanged(); } } @@ -424,6 +451,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener { * * @param placeholder the placeholder containing information related to the action it represents */ + @Override public void registerSharedActionPlaceholder(SharedDockingActionPlaceholder placeholder) { String name = placeholder.getName(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolBarUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolBarUtils.java new file mode 100644 index 0000000000..9db8af21b1 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolBarUtils.java @@ -0,0 +1,112 @@ +/* ### + * 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.menu; + +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +import javax.swing.JButton; +import javax.swing.KeyStroke; + +import org.apache.commons.lang3.StringUtils; + +import docking.action.DockingActionIf; +import ghidra.docking.util.DockingWindowsLookAndFeelUtils; +import ghidra.util.StringUtilities; + +class DockingToolBarUtils { + + private static final String START_KEYBINDING_TEXT = "


    ("; + private static final String END_KEYBINDNIG_TEXT = ")
    "; + + /** + * Sets the given button's tooltip text to match that of the given action + * @param button the button + * @param action the action + */ + static void setToolTipText(JButton button, DockingActionIf action) { + + String toolTipText = getToolTipText(action); + String keyBindingText = getKeyBindingAcceleratorText(button, action.getKeyBinding()); + if (keyBindingText != null) { + button.setToolTipText(combingToolTipTextWithKeyBinding(toolTipText, keyBindingText)); + } + else { + button.setToolTipText(toolTipText); + } + } + + private static String combingToolTipTextWithKeyBinding(String toolTipText, + String keyBindingText) { + StringBuilder buffy = new StringBuilder(toolTipText); + if (StringUtilities.startsWithIgnoreCase(toolTipText, "")) { + String endHTMLTag = ""; + int closeTagIndex = StringUtils.indexOfIgnoreCase(toolTipText, endHTMLTag); + if (closeTagIndex < 0) { + // no closing tag, which is acceptable + buffy.append(START_KEYBINDING_TEXT) + .append(keyBindingText) + .append(END_KEYBINDNIG_TEXT); + } + else { + // remove the closing tag, put on our text, and then put the tag back on + buffy.delete(closeTagIndex, closeTagIndex + endHTMLTag.length() + 1); + buffy.append(START_KEYBINDING_TEXT) + .append(keyBindingText) + .append(END_KEYBINDNIG_TEXT) + .append(endHTMLTag); + } + return buffy.toString(); + } + + // plain text (not HTML) + return toolTipText + " (" + keyBindingText + ")"; + } + + private static String getToolTipText(DockingActionIf action) { + String description = action.getDescription(); + if (!StringUtils.isEmpty(description)) { + return description; + } + return action.getName(); + } + + private static String getKeyBindingAcceleratorText(JButton button, KeyStroke keyStroke) { + if (keyStroke == null) { + return null; + } + + // This code is based on that of BasicMenuItemUI + StringBuilder builder = new StringBuilder(); + int modifiers = keyStroke.getModifiers(); + if (modifiers > 0) { + builder.append(InputEvent.getModifiersExText(modifiers)); + + // The Aqua LaF does not use the '+' symbol between modifiers + if (!DockingWindowsLookAndFeelUtils.isUsingAquaUI(button.getUI())) { + builder.append('+'); + } + } + int keyCode = keyStroke.getKeyCode(); + if (keyCode != 0) { + builder.append(KeyEvent.getKeyText(keyCode)); + } + else { + builder.append(keyStroke.getKeyChar()); + } + return builder.toString(); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolbarButton.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolbarButton.java index 212d9b79f6..4a13154a12 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolbarButton.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolbarButton.java @@ -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. @@ -17,6 +16,7 @@ package docking.menu; import java.awt.event.*; +import java.beans.PropertyChangeEvent; import docking.DockingWindowManager; import docking.EmptyBorderToggleButton; @@ -27,7 +27,7 @@ import docking.action.*; * the override notes below). */ public class DockingToolbarButton extends EmptyBorderToggleButton { - private DockingActionIf dockableAction; + private DockingActionIf dockingAction; private ActionContextProvider contextProvider; public DockingToolbarButton(DockingActionIf action, ActionContextProvider contextProvider) { @@ -36,21 +36,47 @@ public class DockingToolbarButton extends EmptyBorderToggleButton { setFocusable(false); addMouseListener(new MouseOverMouseListener()); action.addPropertyChangeListener(propertyChangeListener); + + // make sure this button gets our specialized tooltip + DockingToolBarUtils.setToolTipText(this, dockingAction); } @Override protected void initFromAction(DockingActionIf action) { - dockableAction = action; + dockingAction = action; super.initFromAction(action); } @Override protected void doActionPerformed(ActionEvent e) { - if (dockableAction instanceof ToggleDockingActionIf) { - ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) dockableAction; + if (dockingAction instanceof ToggleDockingActionIf) { + ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) dockingAction; toggleAction.setSelected(!toggleAction.isSelected()); } - dockableAction.actionPerformed(contextProvider.getActionContext(null)); + dockingAction.actionPerformed(contextProvider.getActionContext(null)); + } + + @Override + protected void doPropertyChange(PropertyChangeEvent e) { + super.doPropertyChange(e); + + String name = e.getPropertyName(); + if (name.equals(DockingActionIf.ENABLEMENT_PROPERTY)) { + setEnabled(((Boolean) e.getNewValue()).booleanValue()); + } + else if (name.equals(DockingActionIf.DESCRIPTION_PROPERTY)) { + DockingToolBarUtils.setToolTipText(this, dockingAction); + } + else if (name.equals(DockingActionIf.TOOLBAR_DATA_PROPERTY)) { + ToolBarData toolBarData = (ToolBarData) e.getNewValue(); + setIcon(toolBarData == null ? null : toolBarData.getIcon()); + } + else if (name.equals(ToggleDockingActionIf.SELECTED_STATE_PROPERTY)) { + setSelected((Boolean) e.getNewValue()); + } + else if (name.equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) { + DockingToolBarUtils.setToolTipText(this, dockingAction); + } } @Override @@ -58,23 +84,23 @@ public class DockingToolbarButton extends EmptyBorderToggleButton { // toggle buttons or regular non-toggle buttons, which dictates whether this // button is selected (non-toggle buttons are not selectable). protected boolean isButtonSelected() { - if (dockableAction instanceof ToggleDockingAction) { - return ((ToggleDockingAction) dockableAction).isSelected(); + if (dockingAction instanceof ToggleDockingAction) { + return ((ToggleDockingAction) dockingAction).isSelected(); } return false; } public DockingActionIf getDockingAction() { - return dockableAction; + return dockingAction; } @Override // overridden to reflect the potentiality that our action is a toggle action public void setSelected(boolean b) { - if (dockableAction instanceof ToggleDockingActionIf) { + if (dockingAction instanceof ToggleDockingActionIf) { // only change the state if the action is a toggle action; doing otherwise would // break the DockableAction - ((ToggleDockingActionIf) dockableAction).setSelected(b); + ((ToggleDockingActionIf) dockingAction).setSelected(b); } super.setSelected(b); } @@ -82,15 +108,15 @@ public class DockingToolbarButton extends EmptyBorderToggleButton { @Override // overridden to reflect the potentiality that our action is a toggle action public boolean isSelected() { - if (dockableAction instanceof ToggleDockingActionIf) { - return ((ToggleDockingActionIf) dockableAction).isSelected(); + if (dockingAction instanceof ToggleDockingActionIf) { + return ((ToggleDockingActionIf) dockingAction).isSelected(); } return super.isSelected(); } @Override public void removeListeners() { - dockableAction.removePropertyChangeListener(propertyChangeListener); + dockingAction.removePropertyChangeListener(propertyChangeListener); super.removeListeners(); } @@ -99,7 +125,7 @@ public class DockingToolbarButton extends EmptyBorderToggleButton { @Override public void mouseEntered(MouseEvent me) { - DockingWindowManager.setMouseOverAction(dockableAction); + DockingWindowManager.setMouseOverAction(dockingAction); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/menu/ToolBarItemManager.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/ToolBarItemManager.java index b05db19667..6888ce1d37 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/menu/ToolBarItemManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/ToolBarItemManager.java @@ -19,23 +19,17 @@ import java.awt.event.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import javax.swing.*; - -import org.apache.commons.lang3.StringUtils; +import javax.swing.JButton; +import javax.swing.SwingUtilities; import docking.*; import docking.action.*; -import ghidra.docking.util.DockingWindowsLookAndFeelUtils; -import ghidra.util.StringUtilities; /** * Class to manager toolbar buttons. */ public class ToolBarItemManager implements PropertyChangeListener, ActionListener, MouseListener { - private static final String START_KEYBINDING_TEXT = "

    ("; - private static final String END_KEYBINDNIG_TEXT = ")
    "; - private DockingActionIf toolBarAction; private JButton toolBarButton; private final DockingWindowManager windowManager; @@ -51,15 +45,13 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene action.addPropertyChangeListener(this); } - /** - * Returns the group for this item. - */ String getGroup() { return toolBarAction.getToolBarData().getToolBarGroup(); } /** - * Returns a button for this items action. + * Returns a button for this items action + * @return the button */ public JButton getButton() { if (toolBarButton == null) { @@ -74,81 +66,13 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene button.addActionListener(this); button.addMouseListener(this); button.setName(action.getName()); - setToolTipText(button, action, getToolTipText(action)); + DockingToolBarUtils.setToolTipText(button, action); return button; } - private void setToolTipText(JButton button, DockingActionIf action, String toolTipText) { - String keyBindingText = getKeyBindingAcceleratorText(button, action.getKeyBinding()); - if (keyBindingText != null) { - button.setToolTipText(combingToolTipTextWithKeyBinding(toolTipText, keyBindingText)); - } - else { - button.setToolTipText(toolTipText); - } - javax.swing.ToolTipManager instance = javax.swing.ToolTipManager.sharedInstance(); -// instance.unregisterComponent( button ); - } - - private String combingToolTipTextWithKeyBinding(String toolTipText, String keyBindingText) { - StringBuilder buffy = new StringBuilder(toolTipText); - if (StringUtilities.startsWithIgnoreCase(toolTipText, "")) { - String endHTMLTag = ""; - int closeTagIndex = StringUtils.indexOfIgnoreCase(toolTipText, endHTMLTag); - if (closeTagIndex < 0) { - // no closing tag, which is acceptable - buffy.append(START_KEYBINDING_TEXT).append(keyBindingText).append( - END_KEYBINDNIG_TEXT); - } - else { - // remove the closing tag, put on our text, and then put the tag back on - buffy.delete(closeTagIndex, closeTagIndex + endHTMLTag.length() + 1); - buffy.append(START_KEYBINDING_TEXT).append(keyBindingText).append( - END_KEYBINDNIG_TEXT).append(endHTMLTag); - } - return buffy.toString(); - } - - // plain text (not HTML) - return toolTipText + " (" + keyBindingText + ")"; - } - - private String getToolTipText(DockingActionIf action) { - String description = action.getDescription(); - if (!StringUtils.isEmpty(description)) { - return description; - } - return action.getName(); - } - - private String getKeyBindingAcceleratorText(JButton button, KeyStroke keyStroke) { - if (keyStroke == null) { - return null; - } - - // This code is based on that of BasicMenuItemUI - StringBuilder builder = new StringBuilder(); - int modifiers = keyStroke.getModifiers(); - if (modifiers > 0) { - builder.append(InputEvent.getModifiersExText(modifiers)); - - // The Aqua LaF does not use the '+' symbol between modifiers - if (!DockingWindowsLookAndFeelUtils.isUsingAquaUI(button.getUI())) { - builder.append('+'); - } - } - int keyCode = keyStroke.getKeyCode(); - if (keyCode != 0) { - builder.append(KeyEvent.getKeyText(keyCode)); - } - else { - builder.append(keyStroke.getKeyChar()); - } - return builder.toString(); - } - /** - * Returns the action being managed. + * Returns the action being managed + * @return the action */ public DockingActionIf getAction() { return toolBarAction; @@ -172,7 +96,7 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene toolBarButton.setEnabled(((Boolean) e.getNewValue()).booleanValue()); } else if (name.equals(DockingActionIf.DESCRIPTION_PROPERTY)) { - setToolTipText(toolBarButton, toolBarAction, (String) e.getNewValue()); + DockingToolBarUtils.setToolTipText(toolBarButton, toolBarAction); } else if (name.equals(DockingActionIf.TOOLBAR_DATA_PROPERTY)) { ToolBarData toolBarData = (ToolBarData) e.getNewValue(); @@ -182,7 +106,7 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene toolBarButton.setSelected((Boolean) e.getNewValue()); } else if (name.equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) { - setToolTipText(toolBarButton, toolBarAction, getToolTipText(toolBarAction)); + DockingToolBarUtils.setToolTipText(toolBarButton, toolBarAction); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/test/AbstractDockingTest.java b/Ghidra/Framework/Docking/src/main/java/docking/test/AbstractDockingTest.java index 31eba0046d..1f21d15363 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/test/AbstractDockingTest.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/test/AbstractDockingTest.java @@ -1716,6 +1716,21 @@ public abstract class AbstractDockingTest extends AbstractGenericTest { } } + /** + * Fires a {@link KeyListener#keyPressed(KeyEvent)}, + * {@link KeyListener#keyTyped(KeyEvent)} + * and {@link KeyListener#keyReleased(KeyEvent)} for the given key stroke + * + * @param c the destination component + * @param ks the key stroke + */ + public static void triggerKey(Component c, KeyStroke ks) { + int modifiers = ks.getModifiers(); + char keyChar = ks.getKeyChar(); + int keyCode = ks.getKeyCode(); + triggerKey(c, modifiers, keyCode, keyChar); + } + /** * Fires a {@link KeyListener#keyPressed(KeyEvent)}, {@link KeyListener#keyTyped(KeyEvent)} * and {@link KeyListener#keyReleased(KeyEvent)} for the given key code and char. diff --git a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQ.java b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQ.java index 11a37080cb..f9da6e3646 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQ.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQ.java @@ -136,7 +136,7 @@ import ghidra.util.task.TaskMonitor; * @param The type of the items to be processed. * @param The type of objects resulting from processing an item; if you don't care about the * return value, then make this value whatever you want, like Object or the - * same value as {@link I} and return null from {@link QCallback#process(Object, TaskMonitor)}. + * same value as {@code I} and return null from {@link QCallback#process(Object, TaskMonitor)}. */ public class ConcurrentQ { @@ -452,6 +452,8 @@ public class ConcurrentQ { * You can still call this method to wait for items to be processed, even if you did not * specify to collect results. In that case, the list returned will be empty. * + * @param timeout the timeout + * @param unit the timeout unit * @return the list of QResult objects that have all the results of the completed jobs. * @throws InterruptedException if this call was interrupted. */ @@ -596,7 +598,7 @@ public class ConcurrentQ { if (collectResults) { resultList.add(result); } - tracker.InProgressitemCompletedOrCancelled(); + tracker.inProgressItemCompletedOrCancelled(); fillOpenProcessingSlots(); if (result.hasError() && unhandledException == null) { @@ -729,11 +731,8 @@ public class ConcurrentQ { * Simple connector for traditional TaskMonitor and a task from the ConcurrentQ. This adapter * adds a cancel listener to the TaskMonitor and when cancelled is called on the monitor, * it cancels the currently running (scheduled on the thread pool) and leaves the waiting - * tasks alone. It also implements a QProgressListener and adds itselve to the concurrentQ so + * tasks alone. It also implements a QProgressListener and adds itself to the concurrentQ so * that it gets progress events and messages and sets them on the task monitor. - * - * @param - * @param */ private class QMonitorAdapter implements QProgressListener, CancelledListener { diff --git a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java index e248f89fea..00030e9c41 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java @@ -15,13 +15,12 @@ */ package generic.concurrent; -import ghidra.util.task.TaskMonitor; -import ghidra.util.task.TaskMonitorAdapter; - import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; +import ghidra.util.task.TaskMonitor; + /** * A helper class to build up the potentially complicated {@link ConcurrentQ}. *

    @@ -71,7 +70,7 @@ public class ConcurrentQBuilder { private boolean collectResults; private int maxInProgress; private boolean jobsReportProgress = false; - private TaskMonitor monitor = TaskMonitorAdapter.DUMMY_MONITOR; + private TaskMonitor monitor = TaskMonitor.DUMMY; private boolean cancelClearsAllJobs = true; /** @@ -179,9 +178,14 @@ public class ConcurrentQBuilder { } /** + * Sets whether a cancel will clear all jobs (current and pending) or just the + * current jobs being processed. The default value is {@code true}. + * + * @param clearAllJobs if true, cancelling the monitor will cancel all items currently being + * processed by a thread and clear the scheduled items that haven't yet run. If false, + * only the items currently being processed will be cancelled. + * @return this builder * @see ConcurrentQ#setMonitor(TaskMonitor, boolean) - *

    - * The default value is true. */ public ConcurrentQBuilder setCancelClearsAllJobs(boolean clearAllJobs) { this.cancelClearsAllJobs = clearAllJobs; @@ -191,7 +195,7 @@ public class ConcurrentQBuilder { public ConcurrentQ build(QCallback callback) { ConcurrentQ concurrentQ = - new ConcurrentQ(callback, getQueue(), getThreadPool(), listener, collectResults, + new ConcurrentQ<>(callback, getQueue(), getThreadPool(), listener, collectResults, maxInProgress, jobsReportProgress); if (monitor != null) { @@ -216,6 +220,6 @@ public class ConcurrentQBuilder { if (queue != null) { return queue; } - return new LinkedList(); + return new LinkedList<>(); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ProgressTracker.java b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ProgressTracker.java index d42c75e140..cd05216772 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ProgressTracker.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ProgressTracker.java @@ -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. @@ -60,7 +59,7 @@ class ProgressTracker { } } - void InProgressitemCompletedOrCancelled() { + void inProgressItemCompletedOrCancelled() { lock.lock(); try { completedOrCancelledCount++; diff --git a/Ghidra/Framework/Generic/src/main/java/generic/json/Json.java b/Ghidra/Framework/Generic/src/main/java/generic/json/Json.java new file mode 100644 index 0000000000..2da9ce109f --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/json/Json.java @@ -0,0 +1,139 @@ +/* ### + * 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 generic.json; + +import java.lang.reflect.Field; +import java.util.Arrays; + +import org.apache.commons.lang3.builder.*; + +/** + * A utility class to format strings in JSON format. This is useful for easily generating + * {@code toString()} representations of objects. + */ +public class Json extends ToStringStyle { + + public static final JsonWithNewlinesToStringStyle WITH_NEWLINES = + new JsonWithNewlinesToStringStyle(); + + /** + * A {@link ToStringStyle} inspired by {@link ToStringStyle#JSON_STYLE} that places + * object fields on newlines for more readability + */ + public static class JsonWithNewlinesToStringStyle extends ToStringStyle { + + private JsonWithNewlinesToStringStyle() { + this.setUseClassName(false); + this.setUseIdentityHashCode(false); + + this.setContentStart("{\n\t"); + this.setContentEnd("\n}"); + + this.setArrayStart("["); + this.setArrayEnd("]"); + + this.setFieldSeparator(",\n\t"); + this.setFieldNameValueSeparator(":"); + + this.setNullText("null"); + + this.setSummaryObjectStartText("\"<"); + this.setSummaryObjectEndText(">\""); + + this.setSizeStartText("\"\""); + } + } + + /** + * Creates a Json string representation of the given object and all of its fields. To exclude + * some fields, call {@link #toStringExclude(Object, String...)}. To only include particular + * fields, call {@link #appendToString(StringBuffer, String)}. + * @param o the object + * @return the string + */ + public static String toString(Object o) { + return ToStringBuilder.reflectionToString(o, Json.WITH_NEWLINES); + } + + /** + * Creates a Json string representation of the given object and the given fields + * @param o the object + * @param includFields the fields to include + * @return the string + */ + public static String toString(Object o, String... includFields) { + + InclusiveReflectionToStringBuilder builder = new InclusiveReflectionToStringBuilder(o); + builder.setIncludeFieldNames(includFields); + return builder.toString(); + } + + /** + * Creates a Json string representation of the given object and all of its fields except for + * those in the given exclusion list + * @param o the object + * @param excludedFields the excluded field names + * @return the string + */ + public static String toStringExclude(Object o, String... excludedFields) { + ReflectionToStringBuilder builder = new ReflectionToStringBuilder(o, + Json.WITH_NEWLINES); + builder.setExcludeFieldNames(excludedFields); + return builder.toString(); + } + + // Future: update this class to use the order of the included fields to be the printed ordered + private static class InclusiveReflectionToStringBuilder extends ReflectionToStringBuilder { + + private String[] includedNames; + + public InclusiveReflectionToStringBuilder(Object object) { + super(object, WITH_NEWLINES); + } + + @Override + protected boolean accept(Field field) { + if (!super.accept(field)) { + return false; + } + + if (this.includedNames != null && + Arrays.binarySearch(this.includedNames, field.getName()) >= 0) { + return true; + } + + return false; + } + + /** + * Sets the names to be included + * @param includeFieldNamesParam the names + * @return this builder + */ + public ReflectionToStringBuilder setIncludeFieldNames( + final String... includeFieldNamesParam) { + if (includeFieldNamesParam == null) { + this.includedNames = null; + } + else { + this.includedNames = includeFieldNamesParam; + Arrays.sort(this.includedNames); + } + return this; + } + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java index 69c8ff1613..dc9af4cc34 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java @@ -23,6 +23,8 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import generic.json.Json; + /** * Class with static methods that deal with string manipulation. */ @@ -442,7 +444,6 @@ public class StringUtilities { return true; } - /** * Convert tabs in the given string to spaces using * a default tab width of 8 spaces. @@ -647,7 +648,6 @@ public class StringUtilities { return location.getWord(); } - public static WordLocation findWordLocation(String s, int index, char[] charsToAllow) { int len = s.length(); @@ -774,6 +774,18 @@ public class StringUtilities { return new String(bytes); } + /** + * Creates a JSON string for the given object using all of its fields. To control the + * fields that are in the result string, see {@link Json}. + * + *

    This is here as a marker to point users to the real {@link Json} String utility. + * @param o the object for which to create a string + * @return the string + */ + public static String toStingJson(Object o) { + return Json.toString(o); + } + public static String toStringWithIndent(Object o) { if (o == null) { return "null"; diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TimeoutTaskMonitor.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TimeoutTaskMonitor.java index 46f7dbc7a8..4e3cb9d0b6 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TimeoutTaskMonitor.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TimeoutTaskMonitor.java @@ -25,6 +25,7 @@ import ghidra.util.exception.TimeoutException; import ghidra.util.timer.GTimer; import ghidra.util.timer.GTimerMonitor; import utility.function.Callback; +import utility.function.Dummy; /** * A task monitor that allows clients the ability to specify a timeout after which this monitor @@ -105,7 +106,8 @@ public class TimeoutTaskMonitor implements TaskMonitor { * @param timeoutCallback the callback to call */ public void setTimeoutListener(Callback timeoutCallback) { - this.timeoutCallback = Callback.dummyIfNull(timeoutCallback); + + this.timeoutCallback = Dummy.ifNull(timeoutCallback); } /** diff --git a/Ghidra/Framework/Utility/src/main/java/utility/function/Dummy.java b/Ghidra/Framework/Utility/src/main/java/utility/function/Dummy.java new file mode 100644 index 0000000000..63babef4f8 --- /dev/null +++ b/Ghidra/Framework/Utility/src/main/java/utility/function/Dummy.java @@ -0,0 +1,109 @@ +/* ### + * 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 utility.function; + +import java.util.function.*; + +/** + * A utility class to help create dummy stub functional interfaces + */ +public class Dummy { + + /** + * Creates a dummy callback + * @return a dummy callback + */ + public static Callback callback() { + return () -> { + // no-op + }; + } + + /** + * Creates a dummy consumer + * @return a dummy consumer + */ + public static Consumer consumer() { + return t -> { + // no-op + }; + } + + /** + * Creates a dummy function + * @param the input type + * @param the result type + * @return the function + */ + public static Function function() { + return t -> null; + } + + /** + * Creates a dummy supplier + * @param the result type + * @return the supplier + */ + public static Supplier supplier() { + return () -> null; + } + + /** + * Returns the given consumer object if it is not {@code null}. Otherwise, a {@link #consumer()} + * is returned. This is useful to avoid using {@code null}. + * + * @param c the consumer function to check for {@code null} + * @return a non-null consumer function + */ + public static Consumer ifNull(Consumer c) { + return c == null ? consumer() : c; + } + + /** + * Returns the given callback object if it is not {@code null}. Otherwise, a {@link #callback()} + * is returned. This is useful to avoid using {@code null}. + * + * @param c the callback function to check for {@code null} + * @return a non-null callback function + */ + public static Callback ifNull(Callback c) { + return c == null ? callback() : c; + } + + /** + * Returns the given function object if it is not {@code null}. Otherwise, a + * {@link #function()} is returned. This is useful to avoid using {@code null}. + * + * @param the input type + * @param the result type + * @param f the function to check for {@code null} + * @return a non-null callback function + */ + public static Function ifNull(Function f) { + return f == null ? function() : f; + } + + /** + * Returns the given callback object if it is not {@code null}. Otherwise, a {@link #callback()} + * is returned. This is useful to avoid using {@code null}. + * + * @param s the supplier function to check for {@code null} + * @return a non-null callback function + */ + public static Supplier ifNull(Supplier s) { + return s == null ? supplier() : s; + } +}