Fix for shared global dialog escape action to appear in options without a dialog having been created

This commit is contained in:
dragonmacher 2024-11-22 19:28:30 -05:00
parent f6dfa0964a
commit 739b28f1ad
10 changed files with 325 additions and 84 deletions

View file

@ -0,0 +1,162 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking;
import static org.junit.Assert.*;
import java.awt.Component;
import javax.swing.*;
import org.junit.*;
import docking.action.DockingAction;
import docking.action.KeyBindingData;
import docking.actions.KeyBindingUtils;
import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
import ghidra.framework.plugintool.PluginTool;
import ghidra.test.*;
import ghidra.util.Msg;
import ghidra.util.SpyErrorLogger;
public class DialogComponentProviderActionsTest extends AbstractGhidraHeadedIntegrationTest {
private TestEnv env;
private PluginTool tool;
private DialogComponentProvider provider;
private SpyErrorLogger spyLogger = new SpyErrorLogger();
@Before
public void setUp() throws Exception {
env = new TestEnv();
tool = env.launchDefaultTool();
env.open(new ClassicSampleX86ProgramBuilder().getProgram());
provider = new TestDialogComponentProvider();
Msg.setErrorLogger(spyLogger);
}
@After
public void tearDown() {
env.dispose();
}
@Test
public void testKeyBinding() {
//
// Create an action for the dialog that has a keybinding. Ensure that the action can be
// triggered while the dialog is showing.
//
SpyAction spyAction = new SpyAction();
String ksText = "Control Y";
setKeyBinding(spyAction, ksText);
addAction(spyAction);
// the action should not work if the dialog is not showing
triggerKey(ksText);
assertFalse(spyAction.hasBeenCalled());
showDialogWithoutBlocking(tool, provider);
waitForDialogComponent(provider.getTitle());
triggerKey(ksText);
assertTrue(spyAction.hasBeenCalled());
close(provider);
}
@Test
public void testKeyBinding_SameAsGlobalKeyBinding() {
//
// Create an action for the dialog that has a keybinding. Use a key binding that is the
// same as a global tool action so that both could be triggered. Verify that only the
// dialog's action is executed while the dialog is showing.
//
SpyAction spyAction = new SpyAction();
String ksText = "G";
setKeyBinding(spyAction, ksText);
addAction(spyAction);
// verify the Go To dialog appears (and not Multiple Key Binding Dialog)
triggerKey(ksText);
DialogComponentProvider dialog = waitForDialogComponent("Go To ...");
close(dialog);
showDialogWithoutBlocking(tool, provider);
waitForDialogComponent(provider.getTitle());
triggerKey(ksText);
assertTrue(spyAction.hasBeenCalled());
close(provider);
}
private void addAction(DockingAction action) {
runSwing(() -> provider.addAction(action));
}
private void setKeyBinding(DockingAction action, String ksText) {
runSwing(() -> {
action.setKeyBindingData(new KeyBindingData(ksText));
});
}
private void triggerKey(String ksText) {
Component component = provider.getComponent();
if (!provider.isShowing()) {
CodeBrowserPlugin cbp = env.getPlugin(CodeBrowserPlugin.class);
CodeViewerProvider cvp = cbp.getProvider();
component = cvp.getComponent();
}
KeyStroke ks = KeyBindingUtils.parseKeyStroke(ksText);
triggerKey(component, ks);
waitForSwing();
}
private class SpyAction extends DockingAction {
private volatile boolean hasBeenCalled;
public SpyAction() {
super("Some Dialog Action", "Some Owner");
}
@Override
public void actionPerformed(ActionContext context) {
hasBeenCalled = true;
}
boolean hasBeenCalled() {
return hasBeenCalled;
}
}
private class TestDialogComponentProvider extends DialogComponentProvider {
private JComponent component = new JButton("Hey!");
protected TestDialogComponentProvider() {
super("Test Dialog");
}
@Override
public JComponent getComponent() {
return component;
}
}
}

View file

@ -133,6 +133,9 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
closeAllWindows();
debug("tearDown()"); debug("tearDown()");
env.dispose(); env.dispose();
debug("a"); debug("a");
@ -243,7 +246,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
ToolOptions originalOptions = importOptions(saveFile); ToolOptions originalOptions = importOptions(saveFile);
assertOptionsMatch( assertOptionsMatch(
"The Options objects do not contain different data after changes have been made.", "Options do not contain different data after changes have been made.",
toolKeyBindingOptions, originalOptions); toolKeyBindingOptions, originalOptions);
debug("c"); debug("c");
@ -255,7 +258,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
// verify the changes are different than the original values // verify the changes are different than the original values
assertOptionsDontMatch( assertOptionsDontMatch(
"The Options objects do not contain different data after changes have been made.", "Options does not contain different data after changes have been made.",
toolKeyBindingOptions, originalOptions); toolKeyBindingOptions, originalOptions);
debug("e"); debug("e");
@ -269,7 +272,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
// verify the data is the same as it was before the changes // verify the data is the same as it was before the changes
boolean same = compareOptionsWithKeyStrokeMap(originalOptions, keyStrokeMap); boolean same = compareOptionsWithKeyStrokeMap(originalOptions, keyStrokeMap);
assertTrue("The Options object contains different data than was imported.", same); assertTrue("The exported options contains different data than were imported.", same);
debug("g"); debug("g");
@ -626,23 +629,23 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
// keystrokes (the map is obtained from the key bindings panel after an // keystrokes (the map is obtained from the key bindings panel after an
// import is done). // import is done).
private boolean compareOptionsWithKeyStrokeMap(Options oldOptions, private boolean compareOptionsWithKeyStrokeMap(Options oldOptions,
Map<String, KeyStroke> panelKeyStrokeMap) { Map<String, KeyStroke> currentKsMap) {
List<String> propertyNames = oldOptions.getOptionNames(); List<String> oldNames = oldOptions.getOptionNames();
for (String name : propertyNames) { for (String oldName : oldNames) {
boolean match = panelKeyStrokeMap.containsKey(name); boolean match = currentKsMap.containsKey(oldName);
ActionTrigger actionTrigger = oldOptions.getActionTrigger(name, null); ActionTrigger oldTrigger = oldOptions.getActionTrigger(oldName, null);
KeyStroke optionsKs = null; KeyStroke oldKs = null;
if (actionTrigger != null) { if (oldTrigger != null) {
optionsKs = actionTrigger.getKeyStroke(); oldKs = oldTrigger.getKeyStroke();
} }
KeyStroke panelKs = panelKeyStrokeMap.get(name); KeyStroke currentKs = currentKsMap.get(oldName);
// if the value is null, then it would not have been placed into the options map // if the value is null, then it would not have been placed into the options map
// in the key bindings panel, so we only care about non-null values // in the key bindings panel, so we only care about non-null values
if (optionsKs != null) { if (oldKs != null) {
match &= (optionsKs.equals(panelKs)); match &= (oldKs.equals(currentKs));
} }
else { else {
match = true; match = true;
@ -650,6 +653,18 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
// short-circuit if there are any data that don't match // short-circuit if there are any data that don't match
if (!match) { if (!match) {
boolean containsOption = currentKsMap.containsKey(oldName);
String message = """
Old key stroke does not match new key stroke.
Option: %s
Old: %s
New: %s
Option name in new options?: %s
""".formatted(oldName, oldKs, currentKs, containsOption);
Msg.debug(this, message);
return false; return false;
} }
} }

View file

@ -0,0 +1,40 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking;
import java.awt.Component;
import ghidra.util.Msg;
/**
* Action context for {@link DialogComponentProvider}s.
*/
public class DialogActionContext extends DefaultActionContext {
public DialogActionContext(DialogComponentProvider dialogProvider, Component sourceComponent) {
super(null, dialogProvider, sourceComponent);
}
public DialogComponentProvider getDialogComponentProvider() {
Object contextObject = getContextObject();
if (contextObject instanceof DialogComponentProvider dcp) {
return dcp;
}
Msg.warn(this, "Found dialog context without a DialogComponentProvider context object");
return null;
}
}

View file

@ -28,13 +28,14 @@ import org.jdesktop.animation.timing.TimingTargetAdapter;
import docking.action.*; import docking.action.*;
import docking.action.builder.ActionBuilder; import docking.action.builder.ActionBuilder;
import docking.actions.SharedActionRegistry;
import docking.actions.ToolActions;
import docking.event.mouse.GMouseListenerAdapter; import docking.event.mouse.GMouseListenerAdapter;
import docking.menu.DialogToolbarButton; import docking.menu.DialogToolbarButton;
import docking.util.AnimationUtils; import docking.util.AnimationUtils;
import docking.widgets.label.GDHtmlLabel; import docking.widgets.label.GDHtmlLabel;
import generic.theme.GColor; import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors.Messages; import generic.theme.GThemeDefaults.Colors.Messages;
import generic.util.WindowUtilities;
import ghidra.util.*; import ghidra.util.*;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
import ghidra.util.task.*; import ghidra.util.task.*;
@ -48,6 +49,7 @@ import utility.function.Callback;
public class DialogComponentProvider public class DialogComponentProvider
implements ActionContextProvider, StatusListener, TaskListener { implements ActionContextProvider, StatusListener, TaskListener {
private static final String CLOSE_ACTION_NAME = "Close Dialog";
private static final Color FG_COLOR_ALERT = new GColor("color.fg.dialog.status.alert"); private static final Color FG_COLOR_ALERT = new GColor("color.fg.dialog.status.alert");
private static final Color FG_COLOR_ERROR = new GColor("color.fg.dialog.status.error"); private static final Color FG_COLOR_ERROR = new GColor("color.fg.dialog.status.error");
private static final Color FG_COLOR_WARNING = new GColor("color.fg.dialog.status.warning"); private static final Color FG_COLOR_WARNING = new GColor("color.fg.dialog.status.warning");
@ -82,7 +84,6 @@ public class DialogComponentProvider
private TaskMonitorComponent taskMonitorComponent; private TaskMonitorComponent taskMonitorComponent;
private static final KeyStroke ESC_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); private static final KeyStroke ESC_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
private DockingAction closeAction;
private CardLayout progressCardLayout; private CardLayout progressCardLayout;
private JButton defaultButton; private JButton defaultButton;
@ -182,36 +183,29 @@ public class DialogComponentProvider
rootPanel.add(panel, BorderLayout.SOUTH); rootPanel.add(panel, BorderLayout.SOUTH);
} }
installEscapeAction();
doInitialize(); doInitialize();
} }
private void installEscapeAction() { /**
closeAction = new ActionBuilder("Close Dialog", title) * Called by the framework during startup to register actions that are shared throughout the
* tool. See {@link SharedActionRegistry}.
* @param tool the tool
* @param toolActions the class to which the actions should be added
* @param owner the shared action owner
*/
public static void createSharedActions(Tool tool, ToolActions toolActions, String owner) {
DockingAction closeAction = new ActionBuilder(CLOSE_ACTION_NAME, owner)
.sharedKeyBinding() .sharedKeyBinding()
.keyBinding(ESC_KEYSTROKE) .keyBinding(ESC_KEYSTROKE)
.enabledWhen(this::isMyDialog) .withContext(DialogActionContext.class)
.onAction(c -> escapeCallback()) .enabledWhen(c -> c.getDialogComponentProvider() != null)
.onAction(c -> {
DialogComponentProvider dcp = c.getDialogComponentProvider();
dcp.escapeCallback();
})
.build(); .build();
toolActions.addGlobalAction(closeAction);
addAction(closeAction);
}
private boolean isMyDialog(ActionContext c) {
//
// Each dialog registers a shared action bound to Escape. If all dialog actions are
// enabled, then the user will get prompted to pick which dialog to close when pressing
// Escape. Thus, we limit the enablement of each action to be the dialog that contains the
// focused component. We use the action context to find out if this dialog is the active
// dialog.
//
Window window = WindowUtilities.windowForComponent(c.getSourceComponent());
if (!(window instanceof DockingDialog dockingDialog)) {
return false;
}
return dockingDialog.containsProvider(DialogComponentProvider.this);
} }
/** a callback mechanism for children to do work */ /** a callback mechanism for children to do work */
@ -220,13 +214,19 @@ public class DialogComponentProvider
} }
/** /**
* Returns true if the given keystroke is the trigger for this dialog's close action. * Returns true if the given action is one that has been registered by this dialog.
* @param ks the keystroke * @param action the action
* @return true if the given keystroke is the trigger for this dialog's close action * @return true if the given action is one that has been registered by this dialog
*/ */
public boolean isCloseKeyStroke(KeyStroke ks) { public boolean isDialogKeyBindingAction(DockingActionIf action) {
KeyStroke currentCloseKs = closeAction.getKeyBinding(); if (action instanceof DockingActionProxy proxy) {
return Objects.equals(ks, currentCloseKs); return keyBindingProxyActions.contains(proxy);
}
String name = action.getName();
if (name.equals(CLOSE_ACTION_NAME)) {
return true;
}
return false;
} }
public int getId() { public int getId() {
@ -1254,14 +1254,14 @@ public class DialogComponentProvider
} }
if (event == null) { if (event == null) {
return new DefaultActionContext(null, c); return new DialogActionContext(this, c);
} }
Component sourceComponent = event.getComponent(); Component sourceComponent = event.getComponent();
if (sourceComponent != null) { if (sourceComponent != null) {
c = sourceComponent; c = sourceComponent;
} }
return new DefaultActionContext(null, c).setSourceObject(event.getSource()); return new DialogActionContext(this, c).setSourceObject(event.getSource());
} }
/** /**

View file

@ -4,9 +4,9 @@
* 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.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -61,6 +61,10 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
dockingAction.actionPerformed(context); dockingAction.actionPerformed(context);
} }
public List<DockingActionIf> getValidActions(Object source) {
return getActions(); // the action for this class is always enabled and valid
}
protected ActionContext getLocalContext(ComponentProvider localProvider) { protected ActionContext getLocalContext(ComponentProvider localProvider) {
if (localProvider == null) { if (localProvider == null) {
return new DefaultActionContext(); return new DefaultActionContext();

View file

@ -20,10 +20,12 @@ import static docking.KeyBindingPrecedence.*;
import java.awt.*; import java.awt.*;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.awt.event.KeyListener; import java.awt.event.KeyListener;
import java.util.List;
import javax.swing.*; import javax.swing.*;
import javax.swing.text.JTextComponent; import javax.swing.text.JTextComponent;
import docking.action.DockingActionIf;
import docking.action.MultipleKeyAction; import docking.action.MultipleKeyAction;
import docking.actions.KeyBindingUtils; import docking.actions.KeyBindingUtils;
import docking.menu.keys.MenuKeyProcessor; import docking.menu.keys.MenuKeyProcessor;
@ -133,8 +135,7 @@ 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
KeyStroke ks = KeyStroke.getKeyStrokeForEvent(event); if (!isValidContextForAction(event, action)) {
if (!isValidContextForKeyStroke(ks)) {
return false; return false;
} }
@ -224,35 +225,33 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
return wasInProgress; return wasInProgress;
} }
/** private boolean isValidContextForAction(KeyEvent event, DockingKeyBindingAction kbAction) {
* A check to see if a given keystroke is something that should not be processed, depending
* upon the current state of the system.
*
* @param keyStroke The keystroke to check.
* @return true if the caller of this method should handle the keystroke; false if the
* keystroke should be ignored.
*/
private boolean isValidContextForKeyStroke(KeyStroke keyStroke) {
Window activeWindow = focusProvider.getActiveWindow(); Window activeWindow = focusProvider.getActiveWindow();
if (activeWindow instanceof DockingDialog) { if (!(activeWindow instanceof DockingDialog dialog)) {
return true; // allow all non-dialog windows to process events
// The choice to ignore modal dialogs was made long ago. We cannot remember why the
// choice was made, but speculate that odd things can happen when keybindings are
// processed with modal 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;
if (!dialog.isModal()) {
return true;
}
// Allow modal dialogs to process close keystrokes (e.g., ESCAPE) so they can be closed
DialogComponentProvider provider = dialog.getComponent();
if (provider.isCloseKeyStroke(keyStroke)) {
return true;
}
return false; // modal dialog; non-escape key
} }
return true; // default case; allow it through
// The choice to ignore modal dialogs was made long ago. We cannot remember why the
// choice was made, but speculate that odd things can happen when keybindings are
// processed with modal dialogs open. For now, do not let key bindings get processed
// for modal dialogs. This can be changed in the future if needed.
if (!dialog.isModal()) {
return true;
}
// Allow modal dialogs to process their own actions
DialogComponentProvider provider = dialog.getComponent();
List<DockingActionIf> actions = kbAction.getValidActions(event.getSource());
if (actions.isEmpty()) {
return false; // no actions; not a valid key stroke for this dialog
}
for (DockingActionIf action : actions) {
if (!provider.isDialogKeyBindingAction(action)) {
return false;
}
}
return true; // all actions belong to the active dialog; this is a valid action
} }
private boolean isSettingKeyBindings(KeyEvent event) { private boolean isSettingKeyBindings(KeyEvent event) {

View file

@ -4,9 +4,9 @@
* 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.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -109,6 +109,22 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
} }
} }
@Override
public List<DockingActionIf> getValidActions(Object source) {
if (ignoreActionWhileMenuShowing()) {
return List.of();
}
List<DockingActionIf> validActions = new ArrayList<>();
List<ExecutableAction> proxyActions = getActionsForCurrentOrDefaultContext(source);
for (ExecutableAction proxy : proxyActions) {
DockingActionIf action = proxy.getAction();
validActions.add(action);
}
return validActions;
}
@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

View file

@ -4,9 +4,9 @@
* 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.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -15,6 +15,7 @@
*/ */
package docking.actions; package docking.actions;
import docking.DialogComponentProvider;
import docking.Tool; import docking.Tool;
import docking.action.DockingActionIf; import docking.action.DockingActionIf;
import docking.tool.ToolConstants; import docking.tool.ToolConstants;
@ -38,5 +39,7 @@ public class SharedActionRegistry {
GTable.createSharedActions(tool, toolActions, ToolConstants.SHARED_OWNER); GTable.createSharedActions(tool, toolActions, ToolConstants.SHARED_OWNER);
GTree.createSharedActions(tool, toolActions, ToolConstants.SHARED_OWNER); GTree.createSharedActions(tool, toolActions, ToolConstants.SHARED_OWNER);
DialogComponentProvider.createSharedActions(tool, toolActions, ToolConstants.SHARED_OWNER);
} }
} }

View file

@ -63,6 +63,7 @@
<logger name="ghidra.program.database" level="DEBUG" /> <logger name="ghidra.program.database" level="DEBUG" />
<logger name="ghidra.program.model.lang.xml" level="DEBUG"/> <logger name="ghidra.program.model.lang.xml" level="DEBUG"/>
<logger name="ghidra.app.plugin.assembler" level="DEBUG" /> <logger name="ghidra.app.plugin.assembler" level="DEBUG" />
<logger name="ghidra.app.plugin.core.debug" level="DEBUG" />
<logger name="ghidra.app.plugin.core.functiongraph" level="DEBUG" /> <logger name="ghidra.app.plugin.core.functiongraph" level="DEBUG" />
<logger name="ghidra.app.plugin.core.string" level="DEBUG" /> <logger name="ghidra.app.plugin.core.string" level="DEBUG" />
<logger name="ghidra.app.plugin.core.libraryidentification" level="INFO"/> <logger name="ghidra.app.plugin.core.libraryidentification" level="INFO"/>

View file

@ -60,7 +60,8 @@
<logger name="ghidra.pcodeCPort.slgh_compile" level="INFO"/> <logger name="ghidra.pcodeCPort.slgh_compile" level="INFO"/>
<logger name="ghidra.program.database" level="DEBUG" /> <logger name="ghidra.program.database" level="DEBUG" />
<logger name="ghidra.program.model.lang.xml" level="DEBUG"/> <logger name="ghidra.program.model.lang.xml" level="DEBUG"/>
<logger name="ghidra.app.plugin.assembler" level="DEBUG" /> <logger name="ghidra.app.plugin.assembler" level="DEBUG" />
<logger name="ghidra.app.plugin.core.debug" level="DEBUG" />
<logger name="ghidra.app.plugin.core.functiongraph" level="DEBUG" /> <logger name="ghidra.app.plugin.core.functiongraph" level="DEBUG" />
<logger name="ghidra.app.plugin.core.string" level="DEBUG" /> <logger name="ghidra.app.plugin.core.string" level="DEBUG" />
<logger name="ghidra.app.plugin.core.libraryidentification" level="INFO"/> <logger name="ghidra.app.plugin.core.libraryidentification" level="INFO"/>