GT-3567 - Fixed keybindings not working in DialogComponentProviders

This commit is contained in:
dragonmacher 2020-03-06 12:15:26 -05:00
parent 7a85fdac25
commit 314100a70c
33 changed files with 922 additions and 362 deletions

View file

@ -15,12 +15,9 @@
*/ */
package ghidra.app.plugin.core.functionwindow; package ghidra.app.plugin.core.functionwindow;
import javax.swing.KeyStroke;
import docking.ComponentProvider; import docking.ComponentProvider;
import docking.ComponentProviderActivationListener; import docking.ComponentProviderActivationListener;
import docking.action.DockingAction; import docking.action.DockingAction;
import docking.action.KeyBindingData;
import ghidra.app.CorePluginPackage; import ghidra.app.CorePluginPackage;
import ghidra.app.events.ProgramClosedPluginEvent; import ghidra.app.events.ProgramClosedPluginEvent;
import ghidra.app.plugin.PluginCategoryNames; 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.plugin.core.functioncompare.actions.CompareFunctionsFromFunctionTableAction;
import ghidra.app.services.FunctionComparisonService; import ghidra.app.services.FunctionComparisonService;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.options.OptionsChangeListener;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.PluginInfo;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginStatus; import ghidra.framework.plugintool.util.PluginStatus;
@ -55,7 +50,7 @@ import ghidra.util.task.SwingUpdateManager;
) )
//@formatter:on //@formatter:on
public class FunctionWindowPlugin extends ProgramPlugin implements DomainObjectListener, public class FunctionWindowPlugin extends ProgramPlugin implements DomainObjectListener,
OptionsChangeListener, ComponentProviderActivationListener { ComponentProviderActivationListener {
private DockingAction selectAction; private DockingAction selectAction;
private DockingAction compareFunctionsAction; private DockingAction compareFunctionsAction;
@ -66,12 +61,7 @@ public class FunctionWindowPlugin extends ProgramPlugin implements DomainObjectL
public FunctionWindowPlugin(PluginTool tool) { public FunctionWindowPlugin(PluginTool tool) {
super(tool, true, false); super(tool, true, false);
swingMgr = new SwingUpdateManager(1000, new Runnable() { swingMgr = new SwingUpdateManager(1000, () -> provider.reload());
@Override
public void run() {
provider.reload();
}
});
} }
@Override @Override
@ -221,20 +211,6 @@ public class FunctionWindowPlugin extends ProgramPlugin implements DomainObjectL
tool.addLocalAction(provider, compareFunctionsAction); 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() { void showFunctions() {
provider.showFunctions(); provider.showFunctions();
} }

View file

@ -51,6 +51,8 @@ public class MakeProgramSelectionAction extends DockingAction {
*/ */
public MakeProgramSelectionAction(String owner, GhidraTable table) { public MakeProgramSelectionAction(String owner, GhidraTable table) {
super("Make Selection", owner, KeyBindingType.SHARED); super("Make Selection", owner, KeyBindingType.SHARED);
this.table = table;
init();
} }
/** /**
@ -65,6 +67,10 @@ public class MakeProgramSelectionAction extends DockingAction {
this.plugin = plugin; this.plugin = plugin;
this.table = table; this.table = table;
init();
}
private void init() {
setPopupMenuData( setPopupMenuData(
new MenuData(new String[] { "Make Selection" }, Icons.MAKE_SELECTION_ICON)); new MenuData(new String[] { "Make Selection" }, Icons.MAKE_SELECTION_ICON));
setToolBarData(new ToolBarData(Icons.MAKE_SELECTION_ICON)); setToolBarData(new ToolBarData(Icons.MAKE_SELECTION_ICON));

View file

@ -169,7 +169,7 @@ public class ComponentProviderActionsTest extends AbstractGhidraHeadedIntegratio
setKeyBindingViaF4Dialog_FromWindowsMenu(newKs); setKeyBindingViaF4Dialog_FromWindowsMenu(newKs);
hideProvider(); hideProvider();
pressKey(CONTROL_T); triggerKey(tool.getToolFrame(), CONTROL_T);
assertProviderIsActive(); 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 // 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 // how the action context is generated (it depends on focus). It is only useful to fail
// here in development mode. // here in development mode.
pressKey(controlEsc); triggerKey(tool.getToolFrame(), controlEsc);
assertProviderIsHidden_InNonBatchMode(); 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() { private DockingActionIf getShowProviderAction() {
DockingActionIf showProviderAction = DockingActionIf showProviderAction =

View file

@ -15,31 +15,33 @@
*/ */
package ghidra.app.tablechooser; package ghidra.app.tablechooser;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList; import java.awt.event.ActionEvent;
import java.util.Arrays; import java.awt.event.KeyEvent;
import java.util.HashMap; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After; import javax.swing.*;
import org.junit.Before;
import org.junit.Test;
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.app.nav.Navigatable;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.DummyPluginTool; import ghidra.framework.plugintool.DummyPluginTool;
import ghidra.program.model.address.Address; import ghidra.program.model.address.Address;
import ghidra.program.model.address.TestAddress; import ghidra.program.model.address.TestAddress;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.ToyProgramBuilder; import ghidra.test.ToyProgramBuilder;
import resources.Icons;
import util.CollectionUtils; import util.CollectionUtils;
public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest { public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest {
@ -48,8 +50,9 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
private static final TestExecutorDecision DEFAULT_DECISION = r -> true; private static final TestExecutorDecision DEFAULT_DECISION = r -> true;
private DummyPluginTool tool; private DummyPluginTool tool;
private TableChooserDialog dialog;
private SpyTableChooserExecutor executor; private SpyTableChooserExecutor executor;
private TableChooserDialog dialog;
private TestAction testAction;
/** Interface for tests to signal what is expected of the executor */ /** Interface for tests to signal what is expected of the executor */
private TestExecutorDecision testDecision = DEFAULT_DECISION; private TestExecutorDecision testDecision = DEFAULT_DECISION;
@ -64,7 +67,6 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
public void tearDown() { public void tearDown() {
runSwing(() -> { runSwing(() -> {
tool.close(); tool.close();
//dialog.close();
}); });
} }
@ -76,6 +78,10 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
Program program = new ToyProgramBuilder("Test", true).getProgram(); Program program = new ToyProgramBuilder("Test", true).getProgram();
Navigatable navigatable = null; Navigatable navigatable = null;
dialog = new TableChooserDialog(tool, executor, program, "Title", navigatable); dialog = new TableChooserDialog(tool, executor, program, "Title", navigatable);
testAction = new TestAction();
dialog.addAction(testAction);
dialog.show(); dialog.show();
loadData(); loadData();
} }
@ -250,10 +256,104 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
assertOnlyExecutedOnce(selected2); 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 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) { private void assertRowCount(int expected) {
int actual = getRowCount(); int actual = getRowCount();
assertEquals("Table model row count is not as expected", expected, actual); assertEquals("Table model row count is not as expected", expected, actual);
@ -400,4 +500,29 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
return getAddress().toString(); 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;
}
}
} }

View file

@ -19,6 +19,7 @@ import static org.junit.Assert.*;
import java.awt.Rectangle; import java.awt.Rectangle;
import java.awt.Window; import java.awt.Window;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
@ -152,6 +153,33 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
debug.close(); 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)' * Test method for 'ghidra.framework.plugintool.dialog.KeyBindingUtils.importKeyBindings(PluginTool)'
*/ */
@ -364,7 +392,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
private void reopenTool(PluginTool tool2) { private void reopenTool(PluginTool tool2) {
runSwing(() -> { runSwing(() -> {
ToolServices services = tool.getProject().getToolServices(); ToolServices services = tool.getProject().getToolServices();
tool = (PluginTool) services.launchTool(tool.getName(), null); tool = services.launchTool(tool.getName(), null);
}); });
assertNotNull(tool); assertNotNull(tool);
} }

View file

@ -23,7 +23,8 @@
// @category Analysis // @category Analysis
import java.util.*; import java.util.Iterator;
import java.util.function.Consumer;
import org.apache.commons.collections4.IteratorUtils; import org.apache.commons.collections4.IteratorUtils;
@ -33,6 +34,8 @@ import ghidra.app.script.GhidraScript;
import ghidra.app.tablechooser.*; import ghidra.app.tablechooser.*;
import ghidra.program.model.address.Address; import ghidra.program.model.address.Address;
import ghidra.program.model.listing.*; import ghidra.program.model.listing.*;
import ghidra.program.model.pcode.HighFunction;
import ghidra.program.model.pcode.PcodeOpAST;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
public class CompareFunctionSizesScript extends GhidraScript { public class CompareFunctionSizesScript extends GhidraScript {
@ -41,51 +44,44 @@ public class CompareFunctionSizesScript extends GhidraScript {
protected void run() throws Exception { protected void run() throws Exception {
if (isRunningHeadless()) { if (isRunningHeadless()) {
printf("This script cannot be run headlessly.\n"); println("This script cannot be run headlessly");
return; return;
} }
DecompilerCallback<FuncBodyData> callback = new DecompilerCallback<FuncBodyData>( TableChooserDialog tableDialog =
createTableChooserDialog(currentProgram.getName() + " function sizes", null);
configureTableColumns(tableDialog);
tableDialog.show();
DecompilerCallback<FuncBodyData> callback = new DecompilerCallback<>(
currentProgram, new CompareFunctionSizesScriptConfigurer(currentProgram)) { currentProgram, new CompareFunctionSizesScriptConfigurer(currentProgram)) {
@Override @Override
public FuncBodyData process(DecompileResults results, TaskMonitor tMonitor) public FuncBodyData process(DecompileResults results, TaskMonitor tMonitor)
throws Exception { throws Exception {
InstructionIterator instIter = currentProgram.getListing().getInstructions(
results.getFunction().getBody(), true); Listing listing = currentProgram.getListing();
int numInstructions = IteratorUtils.size(instIter); Function function = results.getFunction();
//indicate failure of decompilation by having 0 high pcode ops 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; int numHighOps = 0;
if (results.getHighFunction() != null && HighFunction highFunction = results.getHighFunction();
results.getHighFunction().getPcodeOps() != null) { if (highFunction != null) {
numHighOps = IteratorUtils.size(results.getHighFunction().getPcodeOps()); Iterator<PcodeOpAST> ops = highFunction.getPcodeOps();
if (ops != null) {
numHighOps = IteratorUtils.size(ops);
} }
return new FuncBodyData(results.getFunction(), numInstructions, numHighOps); }
return new FuncBodyData(function, numInstructions, numHighOps);
} }
}; };
Set<Function> funcsToDecompile = new HashSet<>(); Consumer<FuncBodyData> consumer = data -> tableDialog.add(data);
FunctionIterator fIter = currentProgram.getFunctionManager().getFunctionsNoStubs(true); FunctionIterator it = currentProgram.getFunctionManager().getFunctionsNoStubs(true);
fIter.forEach(e -> funcsToDecompile.add(e)); ParallelDecompiler.decompileFunctions(callback, currentProgram, it, consumer, monitor);
callback.dispose();
if (funcsToDecompile.isEmpty()) {
popup("No functions to decompile!");
return;
}
List<FuncBodyData> 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);
}
} }
class CompareFunctionSizesScriptConfigurer implements DecompileConfigurer { class CompareFunctionSizesScriptConfigurer implements DecompileConfigurer {
@ -210,7 +206,7 @@ public class CompareFunctionSizesScript extends GhidraScript {
} }
}; };
ColumnDisplay<Integer> highOpsColumn = new AbstractComparableColumnDisplay<Integer>() { ColumnDisplay<Integer> highOpsColumn = new AbstractComparableColumnDisplay<>() {
@Override @Override
public Integer getColumnValue(AddressableRowObject rowObject) { public Integer getColumnValue(AddressableRowObject rowObject) {
@ -223,7 +219,7 @@ public class CompareFunctionSizesScript extends GhidraScript {
} }
}; };
ColumnDisplay<Integer> instructionColumn = new AbstractComparableColumnDisplay<Integer>() { ColumnDisplay<Integer> instructionColumn = new AbstractComparableColumnDisplay<>() {
@Override @Override
public Integer getColumnValue(AddressableRowObject rowObject) { public Integer getColumnValue(AddressableRowObject rowObject) {
@ -236,7 +232,7 @@ public class CompareFunctionSizesScript extends GhidraScript {
} }
}; };
ColumnDisplay<Double> ratioColumn = new AbstractComparableColumnDisplay<Double>() { ColumnDisplay<Double> ratioColumn = new AbstractComparableColumnDisplay<>() {
@Override @Override
public Double getColumnValue(AddressableRowObject rowObject) { public Double getColumnValue(AddressableRowObject rowObject) {

View file

@ -76,7 +76,7 @@ public class FindPotentialDecompilerProblems extends GhidraScript {
return; return;
} }
ParallelDecompiler.decompileFunctions(callback, currentProgram, funcsToDecompile, monitor); ParallelDecompiler.decompileFunctions(callback, funcsToDecompile, monitor);
monitor.checkCanceled(); monitor.checkCanceled();
tableDialog.setMessage("Finished"); tableDialog.setMessage("Finished");
} }

View file

@ -60,7 +60,7 @@ public class FixSwitchStatementsWithDecompiler extends GhidraScript {
Set<Function> functions = instructionsByFunction.keySet(); Set<Function> functions = instructionsByFunction.keySet();
try { try {
ParallelDecompiler.decompileFunctions(callback, currentProgram, functions, monitor); ParallelDecompiler.decompileFunctions(callback, functions, monitor);
} }
finally { finally {
callback.dispose(); callback.dispose();

View file

@ -16,6 +16,7 @@
package ghidra.app.decompiler.parallel; package ghidra.app.decompiler.parallel;
import java.util.*; import java.util.*;
import java.util.function.Consumer;
import generic.concurrent.QCallback; import generic.concurrent.QCallback;
import generic.concurrent.QResult; import generic.concurrent.QResult;
@ -46,7 +47,7 @@ public class ParallelDecompiler {
Listing listing = program.getListing(); Listing listing = program.getListing();
FunctionIterator iterator = listing.getFunctions(addresses, true); FunctionIterator iterator = listing.getFunctions(addresses, true);
List<R> results = decompileFunctions(callback, program, iterator, functionCount, monitor); List<R> results = doDecompileFunctions(callback, iterator, functionCount, monitor);
return results; return results;
} }
@ -54,23 +55,54 @@ public class ParallelDecompiler {
* Decompile the given functions using multiple decompilers * Decompile the given functions using multiple decompilers
* *
* @param callback the callback to be called for each that is processed * @param callback the callback to be called for each that is processed
* @param program the program
* @param functions the functions to decompile * @param functions the functions to decompile
* @param monitor the task monitor * @param monitor the task monitor
* @return the list of client results * @return the list of client results
* @throws InterruptedException if interrupted * @throws InterruptedException if interrupted
* @throws Exception if any other exception occurs * @throws Exception if any other exception occurs
*/ */
public static <R> List<R> decompileFunctions(QCallback<Function, R> callback, Program program, public static <R> List<R> decompileFunctions(QCallback<Function, R> callback,
Collection<Function> functions, TaskMonitor monitor) Collection<Function> functions,
TaskMonitor monitor)
throws InterruptedException, Exception { throws InterruptedException, Exception {
List<R> results = List<R> results =
decompileFunctions(callback, program, functions.iterator(), functions.size(), monitor); doDecompileFunctions(callback, functions.iterator(), functions.size(),
monitor);
return results; return results;
} }
private static <R> List<R> decompileFunctions(QCallback<Function, R> callback, Program program, /**
* Decompile the given functions using multiple decompilers.
*
* <p>Results will be passed to the given consumer as they are produced. Calling this
* method allows you to handle results as they are discovered.
*
* <p><strong>This method will wait for all processing before returning.</strong>
*
* @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 <R> void decompileFunctions(QCallback<Function, R> callback,
Program program,
Iterator<Function> functions, Consumer<R> resultsConsumer,
TaskMonitor monitor)
throws InterruptedException, Exception {
int max = program.getFunctionManager().getFunctionCount();
DecompilerConcurrentQ<Function, R> queue =
new DecompilerConcurrentQ<>(callback, THREAD_POOL_NAME, monitor);
monitor.initialize(max);
queue.process(functions, resultsConsumer);
queue.waitUntilDone();
}
private static <R> List<R> doDecompileFunctions(QCallback<Function, R> callback,
Iterator<Function> functions, int count, TaskMonitor monitor) Iterator<Function> functions, int count, TaskMonitor monitor)
throws InterruptedException, Exception { throws InterruptedException, Exception {

View file

@ -162,7 +162,7 @@ public class DecompilerSwitchAnalyzer extends AbstractAnalyzer {
callback.setTimeout(decompilerTimeoutSecondsOption); callback.setTimeout(decompilerTimeoutSecondsOption);
try { try {
ParallelDecompiler.decompileFunctions(callback, program, functions, monitor); ParallelDecompiler.decompileFunctions(callback, functions, monitor);
} }
finally { finally {
callback.dispose(); callback.dispose();

View file

@ -108,7 +108,7 @@ public class ObjectiveC2_DecompilerMessageAnalyzer extends AbstractAnalyzer {
}; };
try { try {
ParallelDecompiler.decompileFunctions(callback, program, functions, monitor); ParallelDecompiler.decompileFunctions(callback, functions, monitor);
} }
finally { finally {
callback.dispose(); callback.dispose();

View file

@ -62,7 +62,7 @@ public class DecompilerValidator extends PostAnalysisValidator {
try { try {
List<String> results = List<String> results =
ParallelDecompiler.decompileFunctions(callback, program, functions, monitor); ParallelDecompiler.decompileFunctions(callback, functions, monitor);
return processResults(results); return processResults(results);
} }
catch (Exception e) { catch (Exception e) {

View file

@ -15,26 +15,39 @@
*/ */
package ghidra.app.util; package ghidra.app.util;
import java.util.Collection; import java.util.*;
import java.util.Iterator;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import generic.concurrent.*; import generic.concurrent.*;
import ghidra.app.plugin.core.analysis.AutoAnalysisManager; import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.task.TaskMonitor; 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 * A class to perform some of the boilerplate setup of the {@link ConcurrentQ} that is shared
* amongst clients that perform decompilation in parallel. * amongst clients that perform decompilation in parallel.
* *
* <p>This class can be used in a blocking or non-blocking fashion.
*
* <ul>
* <li>For blocking usage, call
* one of the <u>{@code add}</u> methods to put items in the queue and then call
* {@link #waitForResults()}.</li>
* <li>For non-blocking usage, simply call
* {@link #process(Iterator, Consumer)}, passing the consumer of the results.</li>
* </ol>
* <p>
*
* @param <I> The input data needed by the supplied {@link QCallback} * @param <I> The input data needed by the supplied {@link QCallback}
* @param <R> The result data (can be the same as {@link I} if there is no result) returned * @param <R> The result data (can be the same as {@code I} if there is no result) returned
* by the {@link QCallback#process(Object, TaskMonitor)} method. * by the {@link QCallback#process(Object, TaskMonitor)} method.
*/ */
public class DecompilerConcurrentQ<I, R> { public class DecompilerConcurrentQ<I, R> {
private ConcurrentQ<I, R> queue; private ConcurrentQ<I, R> queue;
private Consumer<R> resultConsumer = Dummy.consumer();
public DecompilerConcurrentQ(QCallback<I, R> callback, TaskMonitor monitor) { public DecompilerConcurrentQ(QCallback<I, R> callback, TaskMonitor monitor) {
this(callback, AutoAnalysisManager.getSharedAnalsysThreadPool(), monitor); this(callback, AutoAnalysisManager.getSharedAnalsysThreadPool(), monitor);
@ -51,6 +64,7 @@ public class DecompilerConcurrentQ<I, R> {
.setCollectResults(true) .setCollectResults(true)
.setThreadPool(pool) .setThreadPool(pool)
.setMonitor(monitor) .setMonitor(monitor)
.setListener(new InternalResultListener())
.build(callback); .build(callback);
// @formatter:on // @formatter:on
} }
@ -67,6 +81,18 @@ public class DecompilerConcurrentQ<I, R> {
queue.add(i); 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<I> functions, Consumer<R> consumer) {
this.resultConsumer = Objects.requireNonNull(consumer);
addAll(functions);
}
/** /**
* Waits for all results to be delivered. The client is responsible for processing the * Waits for all results to be delivered. The client is responsible for processing the
* results and handling any exceptions that may have occurred. * results and handling any exceptions that may have occurred.
@ -75,15 +101,12 @@ public class DecompilerConcurrentQ<I, R> {
* @throws InterruptedException if interrupted while waiting * @throws InterruptedException if interrupted while waiting
*/ */
public Collection<QResult<I, R>> waitForResults() throws InterruptedException { public Collection<QResult<I, R>> waitForResults() throws InterruptedException {
Collection<QResult<I, R>> results = null;
try { try {
results = queue.waitForResults(); return queue.waitForResults();
} }
finally { finally {
queue.dispose(); queue.dispose();
} }
return results;
} }
/** /**
@ -95,8 +118,13 @@ public class DecompilerConcurrentQ<I, R> {
* @throws Exception any exception that is encountered while processing items. * @throws Exception any exception that is encountered while processing items.
*/ */
public void waitUntilDone() throws InterruptedException, Exception { public void waitUntilDone() throws InterruptedException, Exception {
try {
queue.waitUntilDone(); queue.waitUntilDone();
} }
finally {
queue.dispose();
}
}
public void dispose() { public void dispose() {
queue.dispose(); queue.dispose();
@ -125,4 +153,23 @@ public class DecompilerConcurrentQ<I, R> {
"Unable to shutdown all tasks in " + timeoutSeconds + " " + TimeUnit.SECONDS); "Unable to shutdown all tasks in " + timeoutSeconds + " " + TimeUnit.SECONDS);
} }
} }
private class InternalResultListener implements QItemListener<I, R> {
@Override
public void itemProcessed(QResult<I, R> 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();
}
}
}
} }

View file

@ -59,7 +59,7 @@ public class DecompilerDataTypeReferenceFinder implements DataTypeReferenceFinde
Set<Function> functions = filterFunctions(program, dataType, monitor); Set<Function> functions = filterFunctions(program, dataType, monitor);
try { try {
ParallelDecompiler.decompileFunctions(qCallback, program, functions, monitor); ParallelDecompiler.decompileFunctions(qCallback, functions, monitor);
} }
catch (InterruptedException e) { catch (InterruptedException e) {
Thread.currentThread().interrupt(); // reset the flag Thread.currentThread().interrupt(); // reset the flag
@ -83,7 +83,7 @@ public class DecompilerDataTypeReferenceFinder implements DataTypeReferenceFinde
Set<Function> functions = filterFunctions(program, dataType, monitor); Set<Function> functions = filterFunctions(program, dataType, monitor);
try { try {
ParallelDecompiler.decompileFunctions(qCallback, program, functions, monitor); ParallelDecompiler.decompileFunctions(qCallback, functions, monitor);
} }
catch (InterruptedException e) { catch (InterruptedException e) {
Thread.currentThread().interrupt(); // reset the flag Thread.currentThread().interrupt(); // reset the flag

View file

@ -26,9 +26,7 @@ import org.apache.commons.lang3.StringUtils;
import org.jdesktop.animation.timing.Animator; import org.jdesktop.animation.timing.Animator;
import org.jdesktop.animation.timing.TimingTargetAdapter; import org.jdesktop.animation.timing.TimingTargetAdapter;
import docking.action.ActionContextProvider; import docking.action.*;
import docking.action.DockingActionIf;
import docking.actions.ActionAdapter;
import docking.actions.KeyBindingUtils; import docking.actions.KeyBindingUtils;
import docking.event.mouse.GMouseListenerAdapter; import docking.event.mouse.GMouseListenerAdapter;
import docking.help.HelpService; import docking.help.HelpService;
@ -627,7 +625,7 @@ public class DialogComponentProvider
public void setStatusText(String message, MessageType type, boolean alert) { public void setStatusText(String message, MessageType type, boolean alert) {
String text = StringUtils.isBlank(message) ? " " : message; 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) { private void doSetStatusText(String text, MessageType type, boolean alert) {
@ -659,7 +657,7 @@ public class DialogComponentProvider
*/ */
protected void alertMessage(Callback alertFinishedCallback) { protected void alertMessage(Callback alertFinishedCallback) {
SystemUtilities.runIfSwingOrPostSwingLater(() -> { Swing.runIfSwingOrRunLater(() -> {
doAlertMessage(alertFinishedCallback); doAlertMessage(alertFinishedCallback);
}); });
} }
@ -801,7 +799,7 @@ public class DialogComponentProvider
*/ */
@Override @Override
public void clearStatusText() { public void clearStatusText() {
SystemUtilities.runIfSwingOrPostSwingLater(() -> { Swing.runIfSwingOrRunLater(() -> {
statusLabel.setText(" "); statusLabel.setText(" ");
updateStatusToolTip(); 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<DockingActionIf> getActions() { public Set<DockingActionIf> getActions() {
return new HashSet<>(dialogActions); return new HashSet<>(dialogActions);
} }
@ -1203,36 +1189,22 @@ public class DialogComponentProvider
actionMap.put(action, button); actionMap.put(action, button);
} }
private void registerActionKeyBinding(DockingActionIf dockingAction) { /**
String name = dockingAction.getName(); * Add an action to this dialog. Only actions with icons are added to the toolbar.
KeyStroke stroke = dockingAction.getKeyBinding(); * Note, if you add an action to this dialog, do not also add the action to
if (stroke == null) { * the tool, as this dialog will do that for you.
return; * @param action the action
} */
ActionAdapter actionAdapter = new ActionAdapter(dockingAction, this); public void addAction(final DockingActionIf action) {
Object binding = null; // old binding for keyStroke; dialogActions.add(action);
addToolbarAction(action);
popupManager.addAction(action);
InputMap imap = rootPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); // add the action to the tool in order get key event management (key bindings
if (imap != null) { // options and key event processing)
binding = imap.get(stroke); DockingWindowManager dwm = DockingWindowManager.getActiveInstance();
imap.put(stroke, name); Tool tool = dwm.getTool();
} tool.addAction(new DialogActionProxy(action));
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);
}
} }
public void removeAction(DockingActionIf 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 { public DialogActionProxy(DockingActionIf dockingAction) {
try { super(dockingAction);
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"));
} }
@Override @Override
protected void okCallback() { public boolean isAddToPopup(ActionContext context) {
this.setStatusText("OK"); return false;
// Task t = new BusyTask();
// executeProgressTask(t, 500);
setOkEnabled(false);
} }
@Override @Override
protected void cancelCallback() { public ToolBarData getToolBarData() {
this.setStatusText("Cancel"); return null;
clearScheduledTask();
super.cancelCallback();
} }
} }
} }

View file

@ -1245,7 +1245,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
root.update(); // do this before rebuilding the menu, as new windows may be opened root.update(); // do this before rebuilding the menu, as new windows may be opened
buildComponentMenu(); buildComponentMenu();
SystemUtilities.runSwingLater(() -> updateFocus()); Swing.runLater(() -> updateFocus());
} }
private void updateFocus() { private void updateFocus() {
@ -1292,7 +1292,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
return; return;
} }
SystemUtilities.runSwingLater(() -> { Swing.runLater(() -> {
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
Window activeWindow = kfm.getActiveWindow(); Window activeWindow = kfm.getActiveWindow();
if (activeWindow == null) { 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 // 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, // 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. // 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, private boolean ensureDockableComponentContainsFocusOwner(Component newFocusComponent,
@ -1515,7 +1515,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
// else use last focus component in window // else use last focus component in window
WindowNode node = root.getNodeForWindow(window); WindowNode node = root.getNodeForWindow(window);
if (node == null) { 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 // 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) { private boolean isMyWindow(Window win) {
if (root == null) { if (root == null || win == null) {
return false; return false;
} }
if (root.getMainWindow() == win) {
Window rootFrame = root.getMainWindow();
if (rootFrame == win) {
return true; return true;
} }
Iterator<DetachedWindowNode> iter = root.getDetachedWindows().iterator();
while (iter.hasNext()) { WindowNode node = root.getNodeForWindow(win);
if (iter.next().getWindow() == win) { if (node != null) {
return true; 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; return false;
} }

View file

@ -15,7 +15,6 @@
*/ */
package docking; package docking;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
@ -94,7 +93,7 @@ public class EmptyBorderToggleButton extends EmptyBorderButton {
return isSelected(); 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) { protected void doActionPerformed(ActionEvent e) {
setSelected(!isSelected()); // toggle setSelected(!isSelected()); // toggle
@ -105,7 +104,7 @@ public class EmptyBorderToggleButton extends EmptyBorderButton {
buttonAction.actionPerformed(e); buttonAction.actionPerformed(e);
} }
private void doPropertyChange(PropertyChangeEvent e) { protected void doPropertyChange(PropertyChangeEvent e) {
String name = e.getPropertyName(); String name = e.getPropertyName();
if (name.equals("enabled")) { if (name.equals("enabled")) {
setEnabled(((Boolean) e.getNewValue()).booleanValue()); setEnabled(((Boolean) e.getNewValue()).booleanValue());
@ -165,14 +164,14 @@ public class EmptyBorderToggleButton extends EmptyBorderButton {
return b; return b;
} }
DefaultButtonModel model = (DefaultButtonModel) bm; DefaultButtonModel buttonModel = (DefaultButtonModel) bm;
ButtonGroup group = model.getGroup(); ButtonGroup group = buttonModel.getGroup();
if (group == null) { if (group == null) {
return b; return b;
} }
group.setSelected(model, b); group.setSelected(buttonModel, b);
boolean isSelected = group.isSelected(model); boolean isSelected = group.isSelected(buttonModel);
return isSelected; return isSelected;
} }

View file

@ -127,7 +127,8 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
} }
// some known special cases that we don't wish to process // 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; return false;
} }
@ -218,7 +219,13 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
private boolean isValidContextForKeyStroke(KeyStroke keyStroke) { private boolean isValidContextForKeyStroke(KeyStroke keyStroke) {
Window activeWindow = focusProvider.getActiveWindow(); Window activeWindow = focusProvider.getActiveWindow();
if (activeWindow instanceof DockingDialog) { 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 return true; // default case; allow it through
} }

View file

@ -15,6 +15,7 @@
*/ */
package docking.action; package docking.action;
import java.awt.Window;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.util.*; import java.util.*;
@ -87,19 +88,16 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
*/ */
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
// always return true so we can report the status message // always return true so we can report the status message when all actions are disabled
// when none of the actions is enabled...
return true; return true;
} }
/** /**
* Enables or disables the action. This affects all uses * Enables or disables the action. This affects all uses of the action. Note that for popups,
* of the action. Note that for popups, this affects whether or * this affects whether or not the option is "grayed out", not whether the action is added
* not the option is "grayed out", not whether the action is added
* to the popup. * to the popup.
* *
* @param newValue true to enable the action, false to * @param newValue true to enable the action, false to disable it
* disable it
* @see Action#setEnabled * @see Action#setEnabled
*/ */
@Override @Override
@ -111,17 +109,10 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
} }
} }
/**
* Invoked when an action occurs.
*/
@Override @Override
public void actionPerformed(final ActionEvent event) { public void actionPerformed(final ActionEvent event) {
// Build list of actions which are valid in current context // Build list of actions which are valid in current context
ComponentProvider localProvider = tool.getActiveComponentProvider(); List<ExecutableKeyActionAdapter> list = getActionsForCurrentContext(event.getSource());
ActionContext localContext = getLocalContext(localProvider);
localContext.setSourceObject(event.getSource());
List<ExecutableKeyActionAdapter> list = getValidContextActions(localContext);
// If menu active, disable all key bindings // If menu active, disable all key bindings
if (ignoreActionWhileMenuShowing()) { if (ignoreActionWhileMenuShowing()) {
@ -237,10 +228,7 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
@Override @Override
public KeyBindingPrecedence getKeyBindingPrecedence() { public KeyBindingPrecedence getKeyBindingPrecedence() {
ComponentProvider localProvider = tool.getActiveComponentProvider(); List<ExecutableKeyActionAdapter> validActions = getActionsForCurrentContext(null);
ActionContext localContext = getLocalContext(localProvider);
List<ExecutableKeyActionAdapter> validActions = getValidContextActions(localContext);
if (validActions.isEmpty()) { if (validActions.isEmpty()) {
return null; // a signal that no actions are valid for the current context 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(); return action.getKeyBindingData().getKeyBindingPrecedence();
} }
private List<ExecutableKeyActionAdapter> 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<ExecutableKeyActionAdapter> validActions = getValidContextActions(context);
return validActions;
}
ComponentProvider localProvider = dwm.getActiveComponentProvider();
ActionContext localContext = getLocalContext(localProvider);
localContext.setSourceObject(eventSource);
List<ExecutableKeyActionAdapter> validActions = getValidContextActions(localContext);
return validActions;
}
public List<DockingActionIf> getActions() { public List<DockingActionIf> getActions() {
List<DockingActionIf> list = new ArrayList<>(actions.size()); List<DockingActionIf> list = new ArrayList<>(actions.size());
for (ActionData actionData : actions) { for (ActionData actionData : actions) {

View file

@ -18,6 +18,7 @@ package docking.action;
import javax.swing.Icon; import javax.swing.Icon;
import docking.DockingUtils; import docking.DockingUtils;
import generic.json.Json;
import ghidra.util.SystemUtilities; import ghidra.util.SystemUtilities;
public class ToolBarData { public class ToolBarData {
@ -106,4 +107,9 @@ public class ToolBarData {
ownerAction.firePropertyChanged(DockingActionIf.TOOLBAR_DATA_PROPERTY, oldData, this); ownerAction.firePropertyChanged(DockingActionIf.TOOLBAR_DATA_PROPERTY, oldData, this);
} }
} }
@Override
public String toString() {
return Json.toString(this, "icon", "toolBarGroup", "toolBarSubGroup");
}
} }

View file

@ -27,14 +27,15 @@ import java.util.stream.Collectors;
import javax.swing.*; import javax.swing.*;
import org.apache.commons.collections4.map.LazyMap; import org.apache.commons.collections4.map.LazyMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jdom.*; import org.jdom.*;
import org.jdom.input.SAXBuilder; import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter; import org.jdom.output.XMLOutputter;
import docking.Tool;
import docking.DockingUtils; import docking.DockingUtils;
import docking.Tool;
import docking.action.*; import docking.action.*;
import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooser;
import ghidra.framework.options.ToolOptions; import ghidra.framework.options.ToolOptions;
@ -666,6 +667,8 @@ public class KeyBindingUtils {
* and we want it to look like: "Ctrl-M". * and we want it to look like: "Ctrl-M".
* <br>In Java 1.5.0, Ctrl-M is returned as "ctrl pressed M" * <br>In Java 1.5.0, Ctrl-M is returned as "ctrl pressed M"
* and we want it to look like: "Ctrl-M". * and we want it to look like: "Ctrl-M".
* <br>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 * @param keyStroke the key stroke
* @return the string value; the empty string if the key stroke is null * @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 // get the character used in the key stroke
int firstIndex = keyString.lastIndexOf(' ') + 1; int firstIndex = keyString.lastIndexOf(' ') + 1;
int ctrlIndex = keyString.indexOf(CTRL, firstIndex); int ctrlIndex = indexOf(keyString, CTRL, firstIndex);
if (ctrlIndex >= 0) { if (ctrlIndex >= 0) {
firstIndex = ctrlIndex + CTRL.length(); firstIndex = ctrlIndex + CTRL.length();
} }
int altIndex = keyString.indexOf(ALT, firstIndex); int altIndex = indexOf(keyString, ALT, firstIndex);
if (altIndex >= 0) { if (altIndex >= 0) {
firstIndex = altIndex + ALT.length(); firstIndex = altIndex + ALT.length();
} }
int shiftIndex = keyString.indexOf(SHIFT, firstIndex); int shiftIndex = indexOf(keyString, SHIFT, firstIndex);
if (shiftIndex >= 0) { if (shiftIndex >= 0) {
firstIndex = shiftIndex + SHIFT.length(); firstIndex = shiftIndex + SHIFT.length();
} }
int metaIndex = keyString.indexOf(META, firstIndex); int metaIndex = indexOf(keyString, META, firstIndex);
if (metaIndex >= 0) { if (metaIndex >= 0) {
firstIndex = metaIndex + META.length(); firstIndex = metaIndex + META.length();
} }
@ -714,18 +717,31 @@ public class KeyBindingUtils {
StringBuilder buffy = new StringBuilder(); StringBuilder buffy = new StringBuilder();
if (isShift(modifiers)) { if (isShift(modifiers)) {
buffy.insert(0, SHIFT + MODIFIER_SEPARATOR); buffy.insert(0, SHIFT + MODIFIER_SEPARATOR);
keyString = removeIgnoreCase(keyString, SHIFT);
} }
if (isAlt(modifiers)) { if (isAlt(modifiers)) {
buffy.insert(0, ALT + MODIFIER_SEPARATOR); buffy.insert(0, ALT + MODIFIER_SEPARATOR);
keyString = removeIgnoreCase(keyString, ALT);
} }
if (isControl(modifiers)) { if (isControl(modifiers)) {
buffy.insert(0, CTRL + MODIFIER_SEPARATOR); buffy.insert(0, CTRL + MODIFIER_SEPARATOR);
keyString = removeIgnoreCase(keyString, CONTROL);
} }
if (isMeta(modifiers)) { if (isMeta(modifiers)) {
buffy.insert(0, META + MODIFIER_SEPARATOR); buffy.insert(0, META + MODIFIER_SEPARATOR);
keyString = removeIgnoreCase(keyString, META);
} }
buffy.append(keyString); 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 // ignore the deprecated; remove when we are confident that all tool actions no longer use the

View file

@ -18,6 +18,8 @@ package docking.actions;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.Action; import javax.swing.Action;
import javax.swing.KeyStroke; import javax.swing.KeyStroke;
@ -41,6 +43,9 @@ import util.CollectionUtils;
*/ */
public class ToolActions implements DockingToolActions, PropertyChangeListener { 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; private ActionToGuiHelper actionGuiHelper;
/* /*
@ -57,6 +62,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
private ToolOptions keyBindingOptions; private ToolOptions keyBindingOptions;
private Tool dockingTool; private Tool dockingTool;
private KeyBindingsManager keyBindingsManager; private KeyBindingsManager keyBindingsManager;
private OptionsChangeListener optionChangeListener = (options, optionName, oldValue,
newValue) -> updateKeyBindingsFromOptions(options, optionName, (KeyStroke) newValue);
/** /**
* Construct an ActionManager * Construct an ActionManager
@ -69,6 +76,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
this.actionGuiHelper = actionToGuiHelper; this.actionGuiHelper = actionToGuiHelper;
this.keyBindingsManager = new KeyBindingsManager(tool); this.keyBindingsManager = new KeyBindingsManager(tool);
this.keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS); this.keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
this.keyBindingOptions.addOptionsChangeListener(optionChangeListener);
createReservedKeyBindings(); createReservedKeyBindings();
SharedActionRegistry.installSharedActions(tool, this); SharedActionRegistry.installSharedActions(tool, this);
@ -356,13 +364,33 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
return actionsByNameByOwner.get(owner).get(name); 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<DockingActionIf> 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 @Override
public void propertyChange(PropertyChangeEvent evt) { public void propertyChange(PropertyChangeEvent evt) {
if (!evt.getPropertyName().equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) { if (!evt.getPropertyName().equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) {
return; return;
} }
DockingAction action = (DockingAction) evt.getSource(); DockingActionIf action = (DockingActionIf) evt.getSource();
if (!action.getKeyBindingType().isManaged()) { if (!action.getKeyBindingType().isManaged()) {
// this reads unusually, but we need to notify the tool to rebuild its 'Window' menu // 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 // 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(); newKeyStroke = newKeyBindingData.getKeyBinding();
} }
Options opt = dockingTool.getOptions(DockingToolConstants.KEY_BINDINGS); KeyStroke optKeyStroke = keyBindingOptions.getKeyStroke(action.getFullName(), null);
KeyStroke optKeyStroke = opt.getKeyStroke(action.getFullName(), null);
if (newKeyStroke == null) { if (newKeyStroke == null) {
opt.removeOption(action.getFullName()); keyBindingOptions.removeOption(action.getFullName());
} }
else if (!newKeyStroke.equals(optKeyStroke)) { else if (!newKeyStroke.equals(optKeyStroke)) {
opt.setKeyStroke(action.getFullName(), newKeyStroke); keyBindingOptions.setKeyStroke(action.getFullName(), newKeyStroke);
keyBindingsChanged(); keyBindingsChanged();
} }
} }
@ -424,6 +451,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
* *
* @param placeholder the placeholder containing information related to the action it represents * @param placeholder the placeholder containing information related to the action it represents
*/ */
@Override
public void registerSharedActionPlaceholder(SharedDockingActionPlaceholder placeholder) { public void registerSharedActionPlaceholder(SharedDockingActionPlaceholder placeholder) {
String name = placeholder.getName(); String name = placeholder.getName();

View file

@ -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 = "<BR><HR><CENTER>(";
private static final String END_KEYBINDNIG_TEXT = ")</CENTER>";
/**
* 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, "<HTML>")) {
String endHTMLTag = "</HTML>";
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();
}
}

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +16,7 @@
package docking.menu; package docking.menu;
import java.awt.event.*; import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import docking.DockingWindowManager; import docking.DockingWindowManager;
import docking.EmptyBorderToggleButton; import docking.EmptyBorderToggleButton;
@ -27,7 +27,7 @@ import docking.action.*;
* the override notes below). * the override notes below).
*/ */
public class DockingToolbarButton extends EmptyBorderToggleButton { public class DockingToolbarButton extends EmptyBorderToggleButton {
private DockingActionIf dockableAction; private DockingActionIf dockingAction;
private ActionContextProvider contextProvider; private ActionContextProvider contextProvider;
public DockingToolbarButton(DockingActionIf action, ActionContextProvider contextProvider) { public DockingToolbarButton(DockingActionIf action, ActionContextProvider contextProvider) {
@ -36,21 +36,47 @@ public class DockingToolbarButton extends EmptyBorderToggleButton {
setFocusable(false); setFocusable(false);
addMouseListener(new MouseOverMouseListener()); addMouseListener(new MouseOverMouseListener());
action.addPropertyChangeListener(propertyChangeListener); action.addPropertyChangeListener(propertyChangeListener);
// make sure this button gets our specialized tooltip
DockingToolBarUtils.setToolTipText(this, dockingAction);
} }
@Override @Override
protected void initFromAction(DockingActionIf action) { protected void initFromAction(DockingActionIf action) {
dockableAction = action; dockingAction = action;
super.initFromAction(action); super.initFromAction(action);
} }
@Override @Override
protected void doActionPerformed(ActionEvent e) { protected void doActionPerformed(ActionEvent e) {
if (dockableAction instanceof ToggleDockingActionIf) { if (dockingAction instanceof ToggleDockingActionIf) {
ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) dockableAction; ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) dockingAction;
toggleAction.setSelected(!toggleAction.isSelected()); 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 @Override
@ -58,23 +84,23 @@ public class DockingToolbarButton extends EmptyBorderToggleButton {
// toggle buttons or regular non-toggle buttons, which dictates whether this // toggle buttons or regular non-toggle buttons, which dictates whether this
// button is selected (non-toggle buttons are not selectable). // button is selected (non-toggle buttons are not selectable).
protected boolean isButtonSelected() { protected boolean isButtonSelected() {
if (dockableAction instanceof ToggleDockingAction) { if (dockingAction instanceof ToggleDockingAction) {
return ((ToggleDockingAction) dockableAction).isSelected(); return ((ToggleDockingAction) dockingAction).isSelected();
} }
return false; return false;
} }
public DockingActionIf getDockingAction() { public DockingActionIf getDockingAction() {
return dockableAction; return dockingAction;
} }
@Override @Override
// overridden to reflect the potentiality that our action is a toggle action // overridden to reflect the potentiality that our action is a toggle action
public void setSelected(boolean b) { 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 // only change the state if the action is a toggle action; doing otherwise would
// break the DockableAction // break the DockableAction
((ToggleDockingActionIf) dockableAction).setSelected(b); ((ToggleDockingActionIf) dockingAction).setSelected(b);
} }
super.setSelected(b); super.setSelected(b);
} }
@ -82,15 +108,15 @@ public class DockingToolbarButton extends EmptyBorderToggleButton {
@Override @Override
// overridden to reflect the potentiality that our action is a toggle action // overridden to reflect the potentiality that our action is a toggle action
public boolean isSelected() { public boolean isSelected() {
if (dockableAction instanceof ToggleDockingActionIf) { if (dockingAction instanceof ToggleDockingActionIf) {
return ((ToggleDockingActionIf) dockableAction).isSelected(); return ((ToggleDockingActionIf) dockingAction).isSelected();
} }
return super.isSelected(); return super.isSelected();
} }
@Override @Override
public void removeListeners() { public void removeListeners() {
dockableAction.removePropertyChangeListener(propertyChangeListener); dockingAction.removePropertyChangeListener(propertyChangeListener);
super.removeListeners(); super.removeListeners();
} }
@ -99,7 +125,7 @@ public class DockingToolbarButton extends EmptyBorderToggleButton {
@Override @Override
public void mouseEntered(MouseEvent me) { public void mouseEntered(MouseEvent me) {
DockingWindowManager.setMouseOverAction(dockableAction); DockingWindowManager.setMouseOverAction(dockingAction);
} }
@Override @Override

View file

@ -19,23 +19,17 @@ import java.awt.event.*;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import javax.swing.*; import javax.swing.JButton;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.StringUtils;
import docking.*; import docking.*;
import docking.action.*; import docking.action.*;
import ghidra.docking.util.DockingWindowsLookAndFeelUtils;
import ghidra.util.StringUtilities;
/** /**
* Class to manager toolbar buttons. * Class to manager toolbar buttons.
*/ */
public class ToolBarItemManager implements PropertyChangeListener, ActionListener, MouseListener { public class ToolBarItemManager implements PropertyChangeListener, ActionListener, MouseListener {
private static final String START_KEYBINDING_TEXT = "<BR><HR><CENTER>(";
private static final String END_KEYBINDNIG_TEXT = ")</CENTER>";
private DockingActionIf toolBarAction; private DockingActionIf toolBarAction;
private JButton toolBarButton; private JButton toolBarButton;
private final DockingWindowManager windowManager; private final DockingWindowManager windowManager;
@ -51,15 +45,13 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene
action.addPropertyChangeListener(this); action.addPropertyChangeListener(this);
} }
/**
* Returns the group for this item.
*/
String getGroup() { String getGroup() {
return toolBarAction.getToolBarData().getToolBarGroup(); return toolBarAction.getToolBarData().getToolBarGroup();
} }
/** /**
* Returns a button for this items action. * Returns a button for this items action
* @return the button
*/ */
public JButton getButton() { public JButton getButton() {
if (toolBarButton == null) { if (toolBarButton == null) {
@ -74,81 +66,13 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene
button.addActionListener(this); button.addActionListener(this);
button.addMouseListener(this); button.addMouseListener(this);
button.setName(action.getName()); button.setName(action.getName());
setToolTipText(button, action, getToolTipText(action)); DockingToolBarUtils.setToolTipText(button, action);
return button; 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, "<HTML>")) {
String endHTMLTag = "</HTML>";
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() { public DockingActionIf getAction() {
return toolBarAction; return toolBarAction;
@ -172,7 +96,7 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene
toolBarButton.setEnabled(((Boolean) e.getNewValue()).booleanValue()); toolBarButton.setEnabled(((Boolean) e.getNewValue()).booleanValue());
} }
else if (name.equals(DockingActionIf.DESCRIPTION_PROPERTY)) { 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)) { else if (name.equals(DockingActionIf.TOOLBAR_DATA_PROPERTY)) {
ToolBarData toolBarData = (ToolBarData) e.getNewValue(); ToolBarData toolBarData = (ToolBarData) e.getNewValue();
@ -182,7 +106,7 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene
toolBarButton.setSelected((Boolean) e.getNewValue()); toolBarButton.setSelected((Boolean) e.getNewValue());
} }
else if (name.equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) { else if (name.equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) {
setToolTipText(toolBarButton, toolBarAction, getToolTipText(toolBarAction)); DockingToolBarUtils.setToolTipText(toolBarButton, toolBarAction);
} }
} }

View file

@ -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)} * Fires a {@link KeyListener#keyPressed(KeyEvent)}, {@link KeyListener#keyTyped(KeyEvent)}
* and {@link KeyListener#keyReleased(KeyEvent)} for the given key code and char. * and {@link KeyListener#keyReleased(KeyEvent)} for the given key code and char.

View file

@ -136,7 +136,7 @@ import ghidra.util.task.TaskMonitor;
* @param <I> The type of the items to be processed. * @param <I> The type of the items to be processed.
* @param <R> The type of objects resulting from processing an item; if you don't care about the * @param <R> 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 <code>Object</code> or the * return value, then make this value whatever you want, like <code>Object</code> 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<I, R> { public class ConcurrentQ<I, R> {
@ -452,6 +452,8 @@ public class ConcurrentQ<I, R> {
* You can still call this method to wait for items to be processed, even if you did not * 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. * 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. * @return the list of QResult objects that have all the results of the completed jobs.
* @throws InterruptedException if this call was interrupted. * @throws InterruptedException if this call was interrupted.
*/ */
@ -596,7 +598,7 @@ public class ConcurrentQ<I, R> {
if (collectResults) { if (collectResults) {
resultList.add(result); resultList.add(result);
} }
tracker.InProgressitemCompletedOrCancelled(); tracker.inProgressItemCompletedOrCancelled();
fillOpenProcessingSlots(); fillOpenProcessingSlots();
if (result.hasError() && unhandledException == null) { if (result.hasError() && unhandledException == null) {
@ -729,11 +731,8 @@ public class ConcurrentQ<I, R> {
* Simple connector for traditional TaskMonitor and a task from the ConcurrentQ. This adapter * 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, * 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 * 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. * that it gets progress events and messages and sets them on the task monitor.
*
* @param <I>
* @param <R>
*/ */
private class QMonitorAdapter implements QProgressListener<I>, CancelledListener { private class QMonitorAdapter implements QProgressListener<I>, CancelledListener {

View file

@ -15,13 +15,12 @@
*/ */
package generic.concurrent; package generic.concurrent;
import ghidra.util.task.TaskMonitor;
import ghidra.util.task.TaskMonitorAdapter;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import ghidra.util.task.TaskMonitor;
/** /**
* A helper class to build up the potentially complicated {@link ConcurrentQ}. * A helper class to build up the potentially complicated {@link ConcurrentQ}.
* <P> * <P>
@ -71,7 +70,7 @@ public class ConcurrentQBuilder<I, R> {
private boolean collectResults; private boolean collectResults;
private int maxInProgress; private int maxInProgress;
private boolean jobsReportProgress = false; private boolean jobsReportProgress = false;
private TaskMonitor monitor = TaskMonitorAdapter.DUMMY_MONITOR; private TaskMonitor monitor = TaskMonitor.DUMMY;
private boolean cancelClearsAllJobs = true; private boolean cancelClearsAllJobs = true;
/** /**
@ -179,9 +178,14 @@ public class ConcurrentQBuilder<I, R> {
} }
/** /**
* 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) * @see ConcurrentQ#setMonitor(TaskMonitor, boolean)
* <p>
* The default value is <code>true</code>.
*/ */
public ConcurrentQBuilder<I, R> setCancelClearsAllJobs(boolean clearAllJobs) { public ConcurrentQBuilder<I, R> setCancelClearsAllJobs(boolean clearAllJobs) {
this.cancelClearsAllJobs = clearAllJobs; this.cancelClearsAllJobs = clearAllJobs;
@ -191,7 +195,7 @@ public class ConcurrentQBuilder<I, R> {
public ConcurrentQ<I, R> build(QCallback<I, R> callback) { public ConcurrentQ<I, R> build(QCallback<I, R> callback) {
ConcurrentQ<I, R> concurrentQ = ConcurrentQ<I, R> concurrentQ =
new ConcurrentQ<I, R>(callback, getQueue(), getThreadPool(), listener, collectResults, new ConcurrentQ<>(callback, getQueue(), getThreadPool(), listener, collectResults,
maxInProgress, jobsReportProgress); maxInProgress, jobsReportProgress);
if (monitor != null) { if (monitor != null) {
@ -216,6 +220,6 @@ public class ConcurrentQBuilder<I, R> {
if (queue != null) { if (queue != null) {
return queue; return queue;
} }
return new LinkedList<I>(); return new LinkedList<>();
} }
} }

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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(); lock.lock();
try { try {
completedOrCancelledCount++; completedOrCancelledCount++;

View file

@ -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("\"<size=");
this.setSizeEndText(">\"");
}
}
/**
* 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;
}
}
}

View file

@ -23,6 +23,8 @@ import java.util.regex.Pattern;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import generic.json.Json;
/** /**
* Class with static methods that deal with string manipulation. * Class with static methods that deal with string manipulation.
*/ */
@ -442,7 +444,6 @@ public class StringUtilities {
return true; return true;
} }
/** /**
* Convert tabs in the given string to spaces using * Convert tabs in the given string to spaces using
* a default tab width of 8 spaces. * a default tab width of 8 spaces.
@ -647,7 +648,6 @@ public class StringUtilities {
return location.getWord(); return location.getWord();
} }
public static WordLocation findWordLocation(String s, int index, char[] charsToAllow) { public static WordLocation findWordLocation(String s, int index, char[] charsToAllow) {
int len = s.length(); int len = s.length();
@ -774,6 +774,18 @@ public class StringUtilities {
return new String(bytes); 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}.
*
* <P>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) { public static String toStringWithIndent(Object o) {
if (o == null) { if (o == null) {
return "null"; return "null";

View file

@ -25,6 +25,7 @@ import ghidra.util.exception.TimeoutException;
import ghidra.util.timer.GTimer; import ghidra.util.timer.GTimer;
import ghidra.util.timer.GTimerMonitor; import ghidra.util.timer.GTimerMonitor;
import utility.function.Callback; import utility.function.Callback;
import utility.function.Dummy;
/** /**
* A task monitor that allows clients the ability to specify a timeout after which this monitor * 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 * @param timeoutCallback the callback to call
*/ */
public void setTimeoutListener(Callback timeoutCallback) { public void setTimeoutListener(Callback timeoutCallback) {
this.timeoutCallback = Callback.dummyIfNull(timeoutCallback);
this.timeoutCallback = Dummy.ifNull(timeoutCallback);
} }
/** /**

View file

@ -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 <T> Consumer<T> consumer() {
return t -> {
// no-op
};
}
/**
* Creates a dummy function
* @param <T> the input type
* @param <R> the result type
* @return the function
*/
public static <T, R> Function<T, R> function() {
return t -> null;
}
/**
* Creates a dummy supplier
* @param <T> the result type
* @return the supplier
*/
public static <T> Supplier<T> 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 <T> Consumer<T> ifNull(Consumer<T> 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 <T> the input type
* @param <R> the result type
* @param f the function to check for {@code null}
* @return a non-null callback function
*/
public static <T, R> Function<T, R> ifNull(Function<T, R> 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 <T> Supplier<T> ifNull(Supplier<T> s) {
return s == null ? supplier() : s;
}
}