GP-4267 Quick Action Dialog

This commit is contained in:
ghidragon 2024-01-30 11:57:54 -05:00
parent 21f1a63f51
commit a88106460b
33 changed files with 2206 additions and 65 deletions

View File

@ -365,7 +365,6 @@ src/main/help/help/topics/Intro/images/Empty_ghidra.png||GHIDRA||||END|
src/main/help/help/topics/Intro/images/Err_Dialog.png||GHIDRA||||END|
src/main/help/help/topics/Intro/images/Open_ghidra.png||GHIDRA||||END|
src/main/help/help/topics/Intro/images/Simple_err_dialog.png||GHIDRA||||END|
src/main/help/help/topics/KeyboardNavigation/KeyboardNavigation.htm||GHIDRA||||END|
src/main/help/help/topics/LabelMgrPlugin/FieldNames.htm||GHIDRA||||END|
src/main/help/help/topics/LabelMgrPlugin/Labels.htm||GHIDRA||||END|
src/main/help/help/topics/LabelMgrPlugin/images/AddLabel.png||GHIDRA||||END|

View File

@ -375,7 +375,7 @@
</tocdef> <!-- End Ghidra Support -->
<tocdef id="Keyboard Navigation" sortgroup="gg" text="Keyboard Navigation" target="help/topics/KeyboardNavigation/KeyboardNavigation.htm"/>
<tocdef id="Keyboard Navigation" sortgroup="gg" text="Keyboard Navigation" target="help/topics/KeyboardNavigation/KeyboardNavigation.html"/>
<tocdef id="Undo/Redo" sortgroup="h" text="Undo/Redo" target="help/topics/Tool/Undo_Redo.htm" />
<tocdef id="Glossary" sortgroup="i" text="Glossary" target="help/topics/Glossary/glossary.htm" />
<tocdef id="What's New" sortgroup="j" text="What's New" target="docs/WhatsNew.html" />

View File

@ -1,47 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<META http-equiv="Content-Language" content="en-us">
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<TITLE>Keyboard Navigation</TITLE>
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY>
<BLOCKQUOTE>
<H1>Keyboard Navigation</H1>
<BLOCKQUOTE>
<H2>Component Traversal</H2>
<P>
Ghidra supports standard keyboard navigation using for traversing component focus cycles within
each window. In general, TAB and &lt;CTRL&gt; TAB will transfer focus to the next component. And
&lt;SHIFT&gt; TAB and &lt;CTRL&gt;&lt;SHIFT&gt;TAB will transfer focus to the previous component
in the cycle. TAB and &lt;SHIFT&gt;TAB do not always work as they are sometimes used by
individual components such as text components, but the &lt;CTRL&gt; versions should work
universally.</P>
<P>
</P>Ghidra also provides some handy shortcut keys for navigation:
<UL>
<LI>&lt;CTRL&gt;F3 - Transfer focus to the next window or dialog.
<LI>&lt;CTRL&gt;&lt;SHIFT&gt;F3 - Transfer focus to the previous window or dialog.
<LI>&lt;CTRL&gt;J - Transfer focus (Jump) to the next component provider (titled component).
<LI>&lt;CTRL&gt;&lt;SHIFT&gt;J - Transfer focus (Jump) to the previous component provider.
</UL>
<H2>Actions</H2>
<P>Global menus can be reached using the various accelerator keys such as &lt;ALT&gt;F to access
the File menu.</P>
<P>
Context menus can be invoked using the &lt;SHIFT&gt;F10 key or the dedicated context menu key
available on some keyboards.</P>
<P>
Toolbar actions currently can't be accessed via the keyboard unless a keybinding is assigned to
them.
</P>
</BLOCKQUOTE>
</BODY>
</HTML>

View File

@ -104,34 +104,33 @@ public abstract class GhidraScreenShotGenerator extends AbstractScreenShotGenera
}
// next, try the .gif extension
potentialFile = new File(helpTopic, "images/" + name + ".gif");
File imageDir = new File(helpTopic, "images");
imageDir.mkdirs();
potentialFile = new File(imageDir, name + ".gif");
if (potentialFile.exists()) {
handleGIFImage(potentialFile);
}
// next, how about jpg?
potentialFile = new File(helpTopic, "images/" + name + ".jpg");
potentialFile = new File(imageDir, name + ".jpg");
if (potentialFile.exists()) {
handleJPGImage(potentialFile);
}
// next, look for any matching image, ignoring case
final String nameLowerCase = name.toLowerCase();
File imagesDir = new File(helpTopic, "images");
File[] matchingFiles = imagesDir.listFiles((FileFilter) f -> {
File[] matchingFiles = imageDir.listFiles((FileFilter) f -> {
String filename = f.getName();
String filenameLowerCase = filename.toLowerCase();
return nameLowerCase.equals(filenameLowerCase);
});
if (matchingFiles.length == 1) {
return matchingFiles[0];
if (matchingFiles == null || matchingFiles.length == 0) {
return new File("ImageNotFound/" + name + ".png");
}
if (matchingFiles.length == 0) {
// fail("Unable to find image by name (case-insensitive): " + name + " for test case: " +
// getName());
return new File("ImageNotFound/" + name + ".png");
if (matchingFiles.length == 1) {
return matchingFiles[0];
}
Assert.fail("Found multiple files, ignoring case, that match name: " + name +
@ -213,7 +212,9 @@ public abstract class GhidraScreenShotGenerator extends AbstractScreenShotGenera
assertNotNull("No new image found", image);
Image oldImage = getOldImage(helpTopic, oldImageName);
File imageFile = new File(helpTopic, "/images/" + oldImageName + DEFAULT_FILENAME_SUFFIX);
File imageDir = new File(helpTopic, "images");
imageDir.mkdirs();
File imageFile = new File(imageDir, oldImageName + DEFAULT_FILENAME_SUFFIX);
ImageDialogProvider dialog = new ImageDialogProvider(imageFile, oldImage, image);
dialog.setTitle("help/topics/" + helpTopic.getName() + "/images/" + oldImageName);
showDialog(dialog);

View File

@ -11,6 +11,8 @@ Module.manifest||GHIDRA||||END|
data/ExtensionPoint.manifest||GHIDRA||||END|
data/docking.theme.properties||GHIDRA||||END|
src/main/help/help/TOC_Source.xml||GHIDRA||||END|
src/main/help/help/topics/KeyboardNavigation/KeyboardNavigation.html||GHIDRA||||END|
src/main/help/help/topics/KeyboardNavigation/images/ActionsDialog.png||GHIDRA||||END|
src/main/help/help/topics/Misc/Welcome_to_Help.htm||GHIDRA||||END|
src/main/help/help/topics/Theming/ThemingDeveloperDocs.html||GHIDRA||||END|
src/main/help/help/topics/Theming/ThemingInternals.html||GHIDRA||||END|

View File

@ -42,7 +42,6 @@ color.fg.fieldpanel = color.fg
color.bg.fieldpanel.selection = color.bg.selection
color.bg.fieldpanel.highlight = color.bg.highlight
icon.folder.new = folder_add.png
icon.toggle.expand = expand.gif
icon.toggle.collapse = collapse.gif
@ -159,4 +158,3 @@ color.bg.highlight = #703401 // orangish
color.bg.filechooser.shortcut = [color]system.color.bg.view

View File

@ -0,0 +1,103 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<META http-equiv="Content-Language" content="en-us">
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<TITLE>Keyboard Navigation</TITLE>
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY>
<BLOCKQUOTE>
<H1>Keyboard Navigation</H1>
<BLOCKQUOTE>
<H2>Component Traversal</H2>
<BLOCKQUOTE>
<P>Ghidra supports standard keyboard navigation for traversing component focus
cycles within each window. In general, TAB and &lt;CTRL&gt; TAB will transfer focus to
the next component. And &lt;SHIFT&gt; TAB and &lt;CTRL&gt;&lt;SHIFT&gt;TAB will transfer
focus to the previous component in the cycle. TAB and &lt;SHIFT&gt;TAB do not always work
as they are sometimes used by individual components such as text components, but the
&lt;CTRL&gt; versions should work universally.</P>
<P>Ghidra also provides some handy shortcut keys for navigation:</P>
<UL>
<LI>&lt;CTRL&gt;F3 - Transfer focus to the next window or dialog.</LI>
<LI>&lt;CTRL&gt;&lt;SHIFT&gt;F3 - Transfer focus to the previous window or dialog.</LI>
<LI>&lt;CTRL&gt;J - Transfer focus (Jump) to the next component provider (titled
component).</LI>
<LI>&lt;CTRL&gt;&lt;SHIFT&gt;J - Transfer focus (Jump) to the previous component
provider.</LI>
</UL>
</BLOCKQUOTE>
<H2>Actions</H2>
<BLOCKQUOTE>
<P>Global menus can be reached using the various accelerator keys such as &lt;ALT&gt;F to
access the File menu.</P>
<P>Context menus can be invoked using the &lt;SHIFT&gt;F10 key or the dedicated context
menu key available on some keyboards.</P>
<P>Toolbar actions currently can't be directly accessed via the keyboard unless a
keybinding is assigned to them.</P>
</BLOCKQUOTE>
<H2>Actions Dialog</H2><A NAME="ActionChooserDialog"/>
<BLOCKQUOTE>
<P>The Actions Dialog displays a list of all actions (or commands) relevant to the
currently focused component window or dialog and allows the user to select and invoke any
valid, enabled action using either the mouse or keyboard.</P>
<DIV class="image">
<IMG alt="" src="images/ActionsDialog.png">
</DIV>
<P>The dialog displays a list of available actions, grouped into categories indicating
from where the action is normally invoked. Actions that are not enabled (and therefore
can't be invoked) are displayed as faded.</P>
<P>Initially, the dialog shows just the local toolbar and menu items for the currently
focused component, along with all relevant popup and keyboard actions. Repeatedly
pressing the keybinding assigned to this action (default is &LT;CTRL&GT; 3) will show
additional, less relevant actions.
<P>Displayed actions in the list can be selected via the mouse or by using the up/down
arrows to move the current selection. Pressing the OK button or pressing the
&lt;ENTER&gt; key will cause the dialog to close and the action to be invoked.</P>
<P>Also, the list can be filtered by typing in the provided text box.</P>
<BLOCKQUOTE>
<P><IMG border="0" src="help/shared/note.png" alt="">This dialog can be invoked using
its assigned keybinding (default is &LT;CTRL&GT; 3).</P>
<P><IMG border="0" src="help/shared/note.png" alt="">Help (F1) and Assign Keybinding
(F4) actions work on the selected action.</P>
<P><IMG border="0" src="help/shared/note.png" alt="">Bringing up this dialog and pressing
(&lt;CTRL&gt; 3) a few times to show all local and global actions is a great way to
explore all the actions Ghidra actions. </P>
</BLOCKQUOTE>
</BLOCKQUOTE>
</BLOCKQUOTE>
<BR><BR><BR>
<P style="color:#7f7f7f;">Keywords: find actions, system actions, tool actions, component actions, local
actions, global actions, keyboard actions, list actions, actions list, choose actions,
select actions, display actions, show actions.</P>
</BLOCKQUOTE>
</BODY>
</HTML>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -117,6 +117,11 @@ public abstract class AbstractDockingTool implements Tool {
toolActions.addLocalAction(provider, action);
}
@Override
public Set<DockingActionIf> getLocalActions(ComponentProvider provider) {
return toolActions.getLocalActions(provider);
}
@Override
public void removeLocalAction(ComponentProvider provider, DockingActionIf action) {
toolActions.removeLocalAction(provider, action);
@ -127,6 +132,11 @@ public abstract class AbstractDockingTool implements Tool {
return toolActions.getAllActions();
}
@Override
public Set<DockingActionIf> getGlobalActions() {
return toolActions.getGlobalActions();
}
@Override
public void addPopupActionProvider(PopupActionProvider provider) {
winMgr.addPopupActionProvider(provider);

View File

@ -16,8 +16,10 @@
package docking;
import java.util.Iterator;
import java.util.Set;
import docking.action.DockingActionIf;
import util.CollectionUtils;
/**
* A class that exists primarily to provide access to action-related package-level methods of the
@ -85,4 +87,13 @@ public class ActionToGuiHelper {
public void keyBindingsChanged() {
windowManager.scheduleUpdate();
}
public Set<DockingActionIf> getLocalActions(ComponentProvider provider) {
Iterator<DockingActionIf> actionIterator = windowManager.getComponentActions(provider);
return CollectionUtils.asSet(actionIterator);
}
public Set<DockingActionIf> getGlobalActions() {
return windowManager.getGlobalActions();
}
}

View File

@ -306,6 +306,14 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext
}
}
/**
* Returns all the local actions registered for this component provider.
* @return all the local actions registered for this component provider
*/
public Set<DockingActionIf> getLocalActions() {
return dockingTool.getLocalActions(this);
}
/**
* Removes all local actions from this component provider
*/

View File

@ -2483,6 +2483,14 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
return new DefaultActionContext(provider, null);
}
/**
* Returns the set of global tool actions
* @return the set of global tool actions
*/
public Set<DockingActionIf> getGlobalActions() {
return new HashSet<>(actionToGuiMapper.getGlobalActions());
}
private ActionContext getDefaultContext(Class<? extends ActionContext> contextType) {
ActionContextProvider contextProvider = defaultContextProviderMap.get(contextType);
if (contextProvider != null) {

View File

@ -192,6 +192,26 @@ public interface Tool extends ServiceProvider {
*/
public Set<DockingActionIf> getAllActions();
/**
* Return a set of all global actions in the tool.
*
* <p>
* Note: the result may contain conceptually duplicate actions, which is when multiple actions
* exist that share the same full name (the full name is the action name with the owner name,
* such as "My Action (MyPlugin)".
*
* @return set of all global actions
*/
public Set<DockingActionIf> getGlobalActions();
/**
* Return a set of all local actions for the given {@link ComponentProvider}.
* @param componentProvider the component provider from which to get local actions
*
* @return set of all local actions for the given provider
*/
public Set<DockingActionIf> getLocalActions(ComponentProvider componentProvider);
/**
* Returns all actions for the given owner
*
@ -340,5 +360,4 @@ public interface Tool extends ServiceProvider {
* systems).
*/
public void close();
}

View File

@ -15,9 +15,8 @@
*/
package docking.action;
import java.util.Arrays;
import java.awt.event.KeyEvent;
import java.util.Arrays;
import javax.swing.Icon;
@ -98,6 +97,10 @@ public class MenuData {
return menuPath;
}
/**
* Returns the menu path as a string. This method includes accelerator characters in the path
* @return the menu path as a string
*/
public String getMenuPathAsString() {
if (menuPath == null || menuPath.length == 0) {
return null;
@ -112,6 +115,28 @@ public class MenuData {
return buildy.toString();
}
/**
* Returns the menu path as a string. This method filters accelerator chars('&') from the path.
* @return the menu path as a string without '&' chars
*/
public String getMenuPathDisplayString() {
if (menuPath == null || menuPath.length == 0) {
return null;
}
StringBuilder buildy = new StringBuilder();
for (int i = 0; i < menuPath.length; i++) {
if (i != (menuPath.length - 1)) {
buildy.append(processMenuItemName(menuPath[i]));
buildy.append("->");
}
else {
// the last entry has already had processMenuItemName called on it
buildy.append(menuPath[i]);
}
}
return buildy.toString();
}
public int getMnemonic() {
return mnemonic;
}

View File

@ -0,0 +1,83 @@
/* ###
* 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.action;
import java.awt.KeyboardFocusManager;
import java.awt.Window;
import docking.*;
import docking.actions.dialog.ActionChooserDialog;
import docking.actions.dialog.ActionDisplayLevel;
import docking.tool.ToolConstants;
import generic.util.action.SystemKeyBindings;
import ghidra.util.HelpLocation;
/**
* Action for displaying the {@link ActionChooserDialog}. This action determines the focused
* {@link ComponentProvider} or {@link DialogComponentProvider} and displays the
* {@link ActionChooserDialog} with actions relevant to that focused component.
*/
public class ShowActionChooserDialogAction extends DockingAction {
public ShowActionChooserDialogAction() {
super("Show Action Chooser Dialog", ToolConstants.TOOL_OWNER);
createSystemKeyBinding(SystemKeyBindings.ACTION_CHOOSER_KEY);
setHelpLocation(new HelpLocation("KeyboardNavigation", "ActionChooserDialog"));
}
@Override
public void actionPerformed(ActionContext context) {
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
Window focusedWindow = kfm.getFocusedWindow();
Tool tool = DockingWindowManager.getActiveInstance().getTool();
if (focusedWindow instanceof DockingDialog dialog) {
showActionsDialog(tool, dialog, context);
}
else if (focusedWindow instanceof DockingFrame dockingFrame) {
showActionsDialog(tool, dockingFrame, context);
}
}
private void showActionsDialog(Tool tool, DockingFrame frame, ActionContext context) {
ComponentProvider provider = tool.getWindowManager().getActiveComponentProvider();
ActionChooserDialog actionsDialog = new ActionChooserDialog(tool, provider, context);
tool.showDialog(actionsDialog);
}
private void showActionsDialog(Tool tool, DockingDialog dialog, ActionContext context) {
DialogComponentProvider dialogProvider = dialog.getDialogComponent();
// There is a special case when the active dialog is the ActionChooserDialog.
// Instead of popping up another ActionChooserDialog, we interpret this action's
// keybinding to mean to show even more actions in the current dialog.
if (dialogProvider instanceof ActionChooserDialog actionsDialog) {
ActionDisplayLevel level = actionsDialog.getActionDisplayLevel();
actionsDialog.setActionDisplayLevel(level.getNextLevel());
return;
}
ActionChooserDialog actionsDialog = new ActionChooserDialog(tool, dialogProvider, context);
tool.showDialog(actionsDialog);
}
@Override
public boolean isEnabledForContext(ActionContext context) {
return true;
}
}

View File

@ -43,6 +43,13 @@ public interface DockingToolActions {
*/
public DockingActionIf getLocalAction(ComponentProvider provider, String actionName);
/**
* Gets all the local actions registered for the given ComponentProvider.
* @param provider the ComponentProvider for which to get its local actions
* @return all the local actions registered for the given ComponentProvider
*/
public Set<DockingActionIf> getLocalActions(ComponentProvider provider);
/**
* Removes the given provider's local action
*
@ -92,6 +99,12 @@ public interface DockingToolActions {
*/
public Set<DockingActionIf> getAllActions();
/**
* Returns all global actions known to the tool
* @return the global actions known to the tool
*/
public Set<DockingActionIf> getGlobalActions();
/**
* Allows clients to register an action by using a placeholder. This is useful when
* an API wishes to have a central object (like a plugin) register actions for transient

View File

@ -99,6 +99,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
addSystemAction(new GlobalFocusTraversalAction(FOCUS_NEXT_COMPONENT_KEY, true));
addSystemAction(new GlobalFocusTraversalAction(FOCUS_PREVIOUS_COMPONENT_KEY, false));
addSystemAction(new ShowActionChooserDialogAction());
// helpful debugging actions
addSystemAction(new ShowFocusInfoAction());
addSystemAction(new ShowFocusCycleAction());
@ -259,6 +261,11 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
}
}
@Override
public Set<DockingActionIf> getLocalActions(ComponentProvider provider) {
return actionGuiHelper.getLocalActions(provider);
}
@Override
public synchronized Set<DockingActionIf> getActions(String owner) {
@ -279,6 +286,11 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
return result;
}
@Override
public synchronized Set<DockingActionIf> getGlobalActions() {
return actionGuiHelper.getGlobalActions();
}
@Override
public synchronized Set<DockingActionIf> getAllActions() {

View File

@ -0,0 +1,349 @@
/* ###
* 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.actions.dialog;
import static ghidra.util.HTMLUtilities.*;
import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiPredicate;
import javax.swing.*;
import docking.*;
import docking.action.*;
import docking.actions.KeyBindingUtils;
import docking.widgets.list.GListCellRenderer;
import docking.widgets.searchlist.SearchList;
import docking.widgets.searchlist.SearchListEntry;
import generic.theme.GThemeDefaults.Colors;
import generic.theme.GThemeDefaults.Colors.Messages;
import ghidra.util.HTMLUtilities;
import ghidra.util.Swing;
import resources.Icons;
/**
* Dialog for displaying and invoking docking actions. The dialog will display a mix of local
* and global actions that varies depending on its current {@link ActionDisplayLevel}.
*/
public class ActionChooserDialog extends DialogComponentProvider {
private ActionsModel model;
private SearchList<DockingActionIf> searchList;
private ActionRunner actionRunner;
/**
* Constructor given an ActionsModel.
* @param model the ActionsModel to use in the dialog
*/
public ActionChooserDialog(ActionsModel model) {
super("Action Chooser Dialog");
this.model = model;
addWorkPanel(buildMainPanel());
setPreferredSize(600, 600);
addOKButton();
addCancelButton();
updateTitle();
}
@Override
protected void dialogShown() {
// showing the dialog causes the docking windows system to clear the mouse over action,
// so we need to re-establish the mouse over action after the dialog is shown
Swing.runLater(() -> {
DockingWindowManager.setMouseOverAction(searchList.getSelectedItem());
});
}
/**
* Constructor for when a {@link ComponentProvider} has focus
* @param tool the active tool
* @param provider the ComponentProvider that has focus
* @param context the ActionContext that is active and will be used to invoke the chosen action
*/
public ActionChooserDialog(Tool tool, ComponentProvider provider, ActionContext context) {
this(provider.getLocalActions(), tool.getGlobalActions(), context);
}
/**
* Constructor for when a {@link DialogComponentProvider} has focus
* @param tool the active tool
* @param dialog the DialogComponentProvider that has focus
* @param context the ActionContext that is active and will be used to invoke the chosen action
*/
public ActionChooserDialog(Tool tool, DialogComponentProvider dialog,
ActionContext context) {
this(dialog.getActions(), new HashSet<>(), context);
}
private ActionChooserDialog(Set<DockingActionIf> localActions,
Set<DockingActionIf> globalActions,
ActionContext context) {
this(new ActionsModel(localActions, globalActions, context));
}
/**
* Returns the current {@link ActionDisplayLevel}
* @return the current action display level
*/
public ActionDisplayLevel getActionDisplayLevel() {
return model.getActionDisplayLevel();
}
/**
* Sets the {@link ActionDisplayLevel} for the dialog which determines which actions to display
* @param level the action display level to use.
*/
public void setActionDisplayLevel(ActionDisplayLevel level) {
model.setDisplayLevel(level);
updateTitle();
}
@Override
protected void okCallback() {
DockingActionIf action = searchList.getSelectedItem();
if (action != null) {
actionChosen(action);
}
}
private void updateTitle() {
switch (model.getActionDisplayLevel()) {
case LOCAL:
setTitle("Relevant Actions (" + model.getSize() + ")");
break;
case GLOBAL:
setTitle("All Valid Local and Global Actions (" + model.getSize() + ")");
break;
case ALL:
setTitle("All Local and Global Actions (" + model.getSize() + ")");
break;
}
}
private JComponent buildMainPanel() {
JPanel panel = new JPanel(new BorderLayout());
searchList = new SearchList<DockingActionIf>(model, (a, c) -> actionChosen(a)) {
@Override
protected BiPredicate<DockingActionIf, String> createFilter(String text) {
return new ActionsFilter(text);
}
};
searchList.setSelectionCallback(this::itemSelected);
searchList.setInitialSelection(); // update selection after adding our listener
searchList.setItemRenderer(new ActionRenderer());
panel.add(searchList);
return panel;
}
private void actionChosen(DockingActionIf action) {
if (!canPerformAction(action)) {
return;
}
close();
scheduleActionAfterFocusRestored(action);
}
private void scheduleActionAfterFocusRestored(DockingActionIf action) {
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
ActionContext context = model.getContext();
actionRunner = new ActionRunner(action, context);
kfm.addPropertyChangeListener("permanentFocusOwner", actionRunner);
}
// for testing
ActionRunner getActionRunner() {
return actionRunner;
}
@Override
public void dispose() {
super.dispose();
searchList.dispose();
}
private boolean canPerformAction(DockingActionIf action) {
if (action == null) {
return false;
}
ActionContext context = model.getContext();
return action.isValidContext(context) && action.isEnabledForContext(context);
}
private void itemSelected(DockingActionIf action) {
// sets the global mouse over action, so that the tool's help action (F1) and
// setKeybinding action (F4) work on the currently selected action in the dialog
DockingWindowManager.setMouseOverAction(action);
setOkEnabled(canPerformAction(action));
}
private static String getActionsDisplayMenuName(DockingActionIf action, MenuData menuData) {
return menuData.getMenuPathDisplayString();
}
private static String colorKeyBindingString(Color color, DockingActionIf action) {
String keyStroke = getKeyBindingString(action);
String keyBindingString = HTMLUtilities.escapeHTML(keyStroke);
String coloredString = HTMLUtilities.colorString(color, keyBindingString);
return HTML_SPACE + HTML_SPACE + coloredString;
}
private static String getKeyBindingString(DockingActionIf action) {
KeyStroke keyBinding = action.getKeyBinding();
if (keyBinding == null) {
return "";
}
return "(" + KeyBindingUtils.parseKeyStroke(keyBinding) + ")";
}
private static String getActionDisplayName(DockingActionIf action, String category) {
ActionGroup group = ActionGroup.getActionByDisplayName(category);
switch (group) {
case LOCAL_MENU:
case GLOBAL_MENU:
return getActionsDisplayMenuName(action, action.getMenuBarData());
case POPUP:
return getActionsDisplayMenuName(action, action.getPopupMenuData());
default:
return action.getName();
}
}
// used for testing
void setFilterText(String string) {
searchList.setFilterText(string);
}
/**
* Class for actually invoking the selected action. Creating an instance of this class
* causes a listener to be added for when focus changes. This is because we don't want
* to invoke the selected action until after this dialog has finished closing and focus
* has been returned to the original component that had focus before this dialog was invoked.
*/
// class not private to allow test access
class ActionRunner implements PropertyChangeListener {
private DockingActionIf action;
private ActionContext context;
ActionRunner(DockingActionIf action, ActionContext context) {
this.action = action;
this.context = context;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
kfm.removePropertyChangeListener("permanentFocusOwner", this);
// we need make sure the focus notification is complete before we perform the action
// in case the action causes a change in focus.
Swing.runLater(() -> activateAction());
}
private void activateAction() {
// Toggle actions do not toggle their state directly, therefore we have to do it
// explicitly before we execute the action.
if (action instanceof ToggleDockingActionIf toggleAction) {
toggleAction.setSelected(!toggleAction.isSelected());
}
action.actionPerformed(context);
}
}
private class ActionRenderer extends GListCellRenderer<SearchListEntry<DockingActionIf>> {
{
setHTMLRenderingEnabled(true);
}
@Override
public Component getListCellRendererComponent(
JList<? extends SearchListEntry<DockingActionIf>> list,
SearchListEntry<DockingActionIf> value, int index, boolean isSelected,
boolean hasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, hasFocus);
DockingActionIf action = value.value();
String category = value.category();
Icon icon = getIcon(action, category);
setText(getHtmlText(action, category, isSelected));
setIcon(icon != null ? icon : Icons.EMPTY_ICON);
return this;
}
private String getHtmlText(DockingActionIf action, String category, boolean isSelected) {
String actionDisplayName = getActionDisplayName(action, category);
String escapedActionName = HTMLUtilities.escapeHTML(actionDisplayName);
String disabledText = "";
StringBuilder builder = new StringBuilder("<html>");
Color fgName = getForeground(); // defaults to list foreground; handles selected state
Color fgKeyBinding = isSelected ? getForeground() : Messages.HINT;
if (!action.isEnabled()) {
fgName = isSelected ? getForeground() : Colors.FOREGROUND_DISABLED;
fgKeyBinding = isSelected ? getForeground() : Colors.FOREGROUND_DISABLED;
disabledText = isSelected ? " <I>disabled</I>" : "";
}
builder.append(HTMLUtilities.colorString(fgName, escapedActionName));
builder.append(colorKeyBindingString(fgKeyBinding, action));
builder.append(disabledText);
return builder.toString();
}
private Icon getIcon(DockingActionIf action, String category) {
ActionGroup group = ActionGroup.getActionByDisplayName(category);
switch (group) {
case LOCAL_TOOLBAR:
case GLOBAL_TOOLBAR:
ToolBarData toolBarData = action.getToolBarData();
return toolBarData != null ? toolBarData.getIcon() : null;
case LOCAL_MENU:
case GLOBAL_MENU:
MenuData menuBarData = action.getMenuBarData();
return menuBarData != null ? menuBarData.getMenuIcon() : null;
case POPUP:
menuBarData = action.getPopupMenuData();
return menuBarData != null ? menuBarData.getMenuIcon() : null;
default:
return null;
}
}
}
private static class ActionsFilter implements BiPredicate<DockingActionIf, String> {
private String filterText;
ActionsFilter(String filterText) {
this.filterText = filterText.toLowerCase();
}
@Override
public boolean test(DockingActionIf t, String category) {
return getActionDisplayName(t, category).toLowerCase().contains(filterText) ||
getKeyBindingString(t).toLowerCase().contains(filterText);
}
}
}

View File

@ -0,0 +1,47 @@
/* ###
* 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.actions.dialog;
/**
* An enum for specifying which actions should be displayed in the {@link ActionChooserDialog}. Each
* successive level is less restrictive and includes more actions to display.
*/
public enum ActionDisplayLevel {
// all local menu and toolbar actions,
// all local and global popup actions with valid context and addToPopup=true,
// all local and global keybinding actions that are valid and enabled
LOCAL,
// adds local and global actions with a valid context, even if disabled
GLOBAL,
// adds local and global actions even if invalid context and disabled
ALL;
public ActionDisplayLevel getNextLevel() {
switch (this) {
case LOCAL:
return GLOBAL;
case GLOBAL:
return ALL;
case ALL:
return LOCAL;
default:
return LOCAL;
}
}
}

View File

@ -0,0 +1,58 @@
/* ###
* 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.actions.dialog;
/**
* This enum defines the actions category groups. Actions displayed in the {@link ActionChooserDialog}
* will be organized into these groups.
*/
public enum ActionGroup {
LOCAL_TOOLBAR("Local Toolbar"),
LOCAL_MENU("Local Menu"),
POPUP("Popup Menu"),
KEYBINDING_ONLY("Keybinding Only"),
GLOBAL_TOOLBAR("Global Toolbar"),
GLOBAL_MENU("Global Menu");
private String displayName;
private ActionGroup(String displayName) {
this.displayName = displayName;
}
/**
* Returns the display name for the action group.
* @return the display name for the action group
*/
public String getDisplayName() {
return displayName;
}
/**
* Returns the ActionGroup that has the given display name.
* @param name the display name for which to find its corresponding group
* @return the ActionGroup that has the given display name
*/
public static ActionGroup getActionByDisplayName(String name) {
ActionGroup[] values = values();
for (ActionGroup group : values) {
if (group.getDisplayName().equals(name)) {
return group;
}
}
return null;
}
}

View File

@ -0,0 +1,229 @@
/* ###
* 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.actions.dialog;
import static docking.actions.dialog.ActionGroup.*;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import docking.*;
import docking.action.DockingActionIf;
import docking.action.MenuData;
import docking.widgets.searchlist.DefaultSearchListModel;
/**
* Model for the SearchList used by the {@link ActionChooserDialog}. This model is constructed
* with two sets of actions; local and global. The local actions are actions that are specific to
* the currently focused {@link ComponentProvider} or {@link DialogComponentProvider}. Global
* actions are actions that are added at the tool level and are not specific to a ComponentProvider
* or DialogComponentProvider.
* <P>
* The model supports the concept of a {@link ActionDisplayLevel}. The display level determines
* which combination of local and global actions to display and takes into account if they are
* valid for the current context, are enabled for the current context and, for popups, the value of
* the "addToPopup" value. Each higher display level is less restrictive and adds more actions in
* the displayed list. See the {@link ActionDisplayLevel} for a description of which actions are
* displayed for each level
*
*/
public class ActionsModel extends DefaultSearchListModel<DockingActionIf> {
private Set<DockingActionIf> localActions;
private Set<DockingActionIf> globalActions;
private ActionDisplayLevel displayLevel = ActionDisplayLevel.LOCAL;
private ActionContext context;
private Comparator<DockingActionIf> nameComparator = new ActionNameComparator();
private Comparator<DockingActionIf> menuPathComparator = new ActionMenuPathComparator();
private Comparator<DockingActionIf> popupPathComparator = new ActionPopupPathComparator();
ActionsModel(Set<DockingActionIf> localActions, Set<DockingActionIf> globalActions,
ActionContext context) {
this.context = context;
this.localActions = localActions;
this.globalActions = globalActions;
populateActions();
}
/**
* Sets the display level for the actions dialog. Each higher level includes more actions
* in the displayed list of actions.
* @param level the {@link ActionDisplayLevel}
*/
public void setDisplayLevel(ActionDisplayLevel level) {
this.displayLevel = level;
populateActions();
fireDataChanged();
}
/**
* Returns the current {@link ActionDisplayLevel} of the dialog.
* @return the current display level of the dialog
*/
public ActionDisplayLevel getActionDisplayLevel() {
return displayLevel;
}
@Override
public void dispose() {
localActions.clear();
globalActions.clear();
context = null;
}
private void populateActions() {
clearData();
switch (displayLevel) {
case LOCAL:
addLocalActions(LOCAL_TOOLBAR, a -> isValidToolbar(a));
addLocalActions(LOCAL_MENU, a -> isValidMenu(a));
addLocalActions(POPUP, a -> isValidPopup(a));
addGlobalActions(POPUP, a -> isValidPopup(a));
addLocalActions(KEYBINDING_ONLY, a -> isValidKeybindingOnly(a));
addGlobalActions(KEYBINDING_ONLY, a -> isValidKeybindingOnly(a));
break;
case GLOBAL:
addLocalActions(LOCAL_TOOLBAR, a -> isValidToolbar(a));
addGlobalActions(GLOBAL_TOOLBAR, a -> isValidToolbar(a));
addLocalActions(LOCAL_MENU, a -> isValidMenu(a));
addGlobalActions(GLOBAL_MENU, a -> isValidMenu(a));
addLocalActions(POPUP, a -> isValidPopup(a));
addGlobalActions(POPUP, a -> isValidPopup(a));
addLocalActions(KEYBINDING_ONLY, a -> isValidKeybindingOnly(a));
addGlobalActions(KEYBINDING_ONLY, a -> isValidKeybindingOnly(a));
break;
case ALL:
addLocalActions(LOCAL_TOOLBAR, a -> isToolbar(a));
addGlobalActions(GLOBAL_TOOLBAR, a -> isToolbar(a));
addLocalActions(LOCAL_MENU, a -> isMenu(a));
addGlobalActions(GLOBAL_MENU, a -> isMenu(a));
addLocalActions(POPUP, a -> isPopup(a));
addGlobalActions(POPUP, a -> isPopup(a));
addLocalActions(KEYBINDING_ONLY, a -> isKeybindingOnly(a));
addGlobalActions(KEYBINDING_ONLY, a -> isKeybindingOnly(a));
break;
}
}
ActionContext getContext() {
return context;
}
private boolean isToolbar(DockingActionIf a) {
return a.getToolBarData() != null;
}
private boolean isValidToolbar(DockingActionIf a) {
return isToolbar(a) && a.isValidContext(context);
}
private boolean isMenu(DockingActionIf a) {
return a.getMenuBarData() != null;
}
private boolean isValidMenu(DockingActionIf a) {
return isMenu(a) && a.isValidContext(context);
}
private boolean isPopup(DockingActionIf a) {
return a.getPopupMenuData() != null;
}
private boolean isValidPopup(DockingActionIf a) {
return isPopup(a) && a.isValidContext(context) && a.isAddToPopup(context);
}
private boolean isKeybindingOnly(DockingActionIf a) {
return a.getToolBarData() == null && a.getMenuBarData() == null &&
a.getPopupMenuData() == null;
}
private boolean isValidKeybindingOnly(DockingActionIf a) {
return isKeybindingOnly(a) && a.isValidContext(context) && a.isEnabledForContext(context);
}
private void addLocalActions(ActionGroup actionGroup, Predicate<DockingActionIf> filter) {
List<DockingActionIf> actions =
localActions.stream().filter(filter).collect(Collectors.toCollection(ArrayList::new));
actions.sort(getSorter(actionGroup));
add(actionGroup.getDisplayName(), actions);
}
private void addGlobalActions(ActionGroup actionGroup, Predicate<DockingActionIf> filter) {
List<DockingActionIf> actions =
globalActions.stream().filter(filter).collect(Collectors.toCollection(ArrayList::new));
actions.sort(getSorter(actionGroup));
add(actionGroup.getDisplayName(), actions);
}
/**
* Returns the appropriate action sorter for the given ActionGroup category. Actions with
* a menu path (menu and popup) use the menu path to sort the actions. All others use
* the action's name for sorting.
* @param actionGroup the type
* @return the comparator to use for sorting actions within their category
*/
private Comparator<? super DockingActionIf> getSorter(ActionGroup actionGroup) {
switch (actionGroup) {
case GLOBAL_MENU:
case LOCAL_MENU:
return menuPathComparator;
case POPUP:
return popupPathComparator;
case GLOBAL_TOOLBAR:
case LOCAL_TOOLBAR:
case KEYBINDING_ONLY:
default:
return nameComparator;
}
}
private class ActionNameComparator implements Comparator<DockingActionIf> {
@Override
public int compare(DockingActionIf a1, DockingActionIf a2) {
return a1.getName().compareTo(a2.getName());
}
}
private class ActionMenuPathComparator implements Comparator<DockingActionIf> {
@Override
public int compare(DockingActionIf a1, DockingActionIf a2) {
MenuData menuData1 = a1.getMenuBarData();
MenuData menuData2 = a2.getMenuBarData();
String path1 = menuData1 != null ? menuData1.getMenuPathAsString() : "";
String path2 = menuData2 != null ? menuData2.getMenuPathAsString() : "";
return path1.compareTo(path2);
}
}
private class ActionPopupPathComparator implements Comparator<DockingActionIf> {
@Override
public int compare(DockingActionIf a1, DockingActionIf a2) {
MenuData menuData1 = a1.getPopupMenuData();
MenuData menuData2 = a2.getPopupMenuData();
String path1 = menuData1 != null ? menuData1.getMenuPathAsString() : "";
String path2 = menuData2 != null ? menuData2.getMenuPathAsString() : "";
return path1.compareTo(path2);
}
}
}

View File

@ -92,6 +92,9 @@ class MenuItemManager implements ManagedMenuItem, PropertyChangeListener, Action
};
}
return e -> {
if (!menuItem.isShowing()) {
return; // model changed, but the user is not moving the mouse
}
boolean isArmed = menuItem.isArmed();
if (isArmed) {
menuHandler.menuItemEntered(action);

View File

@ -0,0 +1,153 @@
/* ###
* 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.widgets.searchlist;
import java.util.*;
import java.util.function.BiPredicate;
import javax.swing.AbstractListModel;
import javax.swing.ListModel;
import utility.function.Dummy;
/**
* Default implementation of the {@link SearchListModel}. Since this model's primary purpose is
* to also implement the {@link ListModel}, this class extends the AbstractListModel.
* This model's primary type is T, but it implements the list model on SearchListEntry<T> to provide
* more information for the custom rendering that groups items into categories.
*
* @param <T> The type of items to display and select.
*/
public class DefaultSearchListModel<T> extends AbstractListModel<SearchListEntry<T>>
implements SearchListModel<T> {
// Use a LinkedHashMap here so that categories are displayed in the order they are added
private Map<String, List<T>> dataMap = new LinkedHashMap<>();
private List<SearchListEntry<T>> displayEntries;
private BiPredicate<T, String> currentFilter = Dummy.biPredicate();
@Override
public int getSize() {
buildEntriesIfNeeded();
return displayEntries.size();
}
@Override
public SearchListEntry<T> getElementAt(int index) {
buildEntriesIfNeeded();
return displayEntries.get(index);
}
/**
* Adds the list of items to the given category. If the category already exists, these items
* will be added to any items already associated with that cateogry.
* @param category the category to add the items to
* @param items the list of items to add to and be associated with the given category
*/
public void add(String category, List<T> items) {
List<T> list = dataMap.computeIfAbsent(category, c -> new ArrayList<T>());
list.addAll(items);
displayEntries = null;
}
/**
* Provides a way to kick the list display to update.
*/
public void fireDataChanged() {
fireContentsChanged(this, 0, getSize());
}
/**
* Removes all categories and items from this model
*/
public void clearData() {
dataMap.clear();
displayEntries = null;
}
@Override
public void setFilter(BiPredicate<T, String> filter) {
this.currentFilter = filter;
displayEntries = null;
rebuildDisplayItems();
fireDataChanged();
}
private void buildEntriesIfNeeded() {
if (displayEntries == null) {
rebuildDisplayItems();
}
}
@Override
public List<String> getCategories() {
return new ArrayList<>(dataMap.keySet());
}
@Override
public void dispose() {
dataMap = null;
displayEntries = null;
}
/**
* Returns a list of all displayed item entries (only ones matching the current filter).
* @return a list of all display item entries
*/
public List<SearchListEntry<T>> getDisplayedItems() {
buildEntriesIfNeeded();
return new ArrayList<>(displayEntries);
}
/**
* Returns a list of all item entries regardless of the current filter.
* @return a list of all item entries
*/
public List<SearchListEntry<T>> getAllItems() {
return getFilteredEntries(Dummy.biPredicate());
}
private void rebuildDisplayItems() {
this.displayEntries = getFilteredEntries(currentFilter);
}
private List<SearchListEntry<T>> getFilteredEntries(BiPredicate<T, String> filter) {
List<SearchListEntry<T>> entries = new ArrayList<>();
for (String category : dataMap.keySet()) {
List<T> list = getFilteredItems(category, filter);
for (T value : list) {
boolean isFirst = list.get(0) == value;
boolean isLast = list.get(list.size() - 1) == value;
entries.add(new SearchListEntry<T>(value, category, isFirst, isLast));
}
}
return entries;
}
private List<T> getFilteredItems(String category, BiPredicate<T, String> filter) {
List<T> filtered = new ArrayList<>();
List<T> list = dataMap.get(category);
for (T value : list) {
if (filter.test(value, category)) {
filtered.add(value);
}
}
return filtered;
}
}

View File

@ -0,0 +1,361 @@
/* ###
* 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.widgets.searchlist;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import java.util.function.*;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.*;
import docking.event.mouse.GMouseListenerAdapter;
import docking.widgets.list.GListCellRenderer;
import utility.function.Dummy;
/**
* Component for displaying and selecting from a filterable list of items that are grouped into
* categories. Similar to a JList, but with filtering and grouping.
*
* @param <T> the type of items in the list
*/
public class SearchList<T> extends JPanel {
private SearchListModel<T> model;
private JList<SearchListEntry<T>> jList;
private int categoryWidth;
private JTextField textField;
private BiConsumer<T, String> chosenItemCallback;
private Consumer<T> selectedConsumer = Dummy.consumer();
private ListCellRenderer<SearchListEntry<T>> itemRenderer = new DefaultItemRenderer();
private String currentFilterText;
/**
* Construct a new SearchList given a model and an chosen item callback.
* @param model the model containing the group list items
* @param chosenItemCallback the callback to be notified when an item is chosen (enter key
* pressed)
*/
public SearchList(SearchListModel<T> model, BiConsumer<T, String> chosenItemCallback) {
super(new BorderLayout());
this.model = model;
this.chosenItemCallback = Dummy.ifNull(chosenItemCallback);
add(buildFilterField(), BorderLayout.NORTH);
add(buildList(), BorderLayout.CENTER);
model.addListDataListener(new SearchListDataListener());
modelChanged();
}
/**
* Returns the current filter text
* @return the current filter text
*/
public String getFilterText() {
return textField.getText();
}
/**
* Sets the current filter text
* @param text the text to set as the current filter
*/
public void setFilterText(String text) {
textField.setText(text);
}
/**
* Gets the currently selected item.
* @return the currently selected item.
*/
public T getSelectedItem() {
SearchListEntry<T> entry = jList.getSelectedValue();
if (entry != null) {
return entry.value();
}
return null;
}
/**
* Sets a consumer to be notified whenever the selected item changes.
* @param consumer the consumer to be notified whenever the selected item changes.
*/
public void setSelectionCallback(Consumer<T> consumer) {
this.selectedConsumer = Dummy.ifNull(consumer);
}
/**
* Sets a custom sub-renderer for displaying list items. Note: this renderer is only used to
* render the item, not the category.
* @param itemRenderer the sub_renderer for rendering the list items, but not the entire line
* which includes the category.
*/
public void setItemRenderer(ListCellRenderer<SearchListEntry<T>> itemRenderer) {
this.itemRenderer = itemRenderer;
}
/**
* Resets the selection to the first element
*/
public void setInitialSelection() {
jList.clearSelection();
if (model.getSize() > 0) {
jList.setSelectedIndex(0);
}
}
/**
* Disposes the component and clears all the model data
*/
public void dispose() {
model.dispose();
}
private Component buildList() {
JPanel panel = new JPanel(new BorderLayout());
panel.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5));
jList = new JList<SearchListEntry<T>>(model);
JScrollPane jScrollPane = new JScrollPane(jList);
jScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
jList.setCellRenderer(new SearchListRenderer());
jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
jList.addKeyListener(new ListKeyListener());
jList.addListSelectionListener(e -> {
if (e.getValueIsAdjusting()) {
return;
}
T selectedItem = getSelectedItem();
selectedConsumer.accept(selectedItem);
});
jList.addMouseListener(new GMouseListenerAdapter() {
@Override
public void doubleClickTriggered(MouseEvent e) {
chooseItem();
}
});
panel.add(jScrollPane, BorderLayout.CENTER);
return panel;
}
private Component buildFilterField() {
JPanel panel = new JPanel(new BorderLayout());
panel.setBorder(BorderFactory.createEmptyBorder(10, 5, 5, 5));
textField = new JTextField();
panel.add(textField, BorderLayout.CENTER);
textField.addKeyListener(new TextFieldKeyListener());
textField.getDocument().addDocumentListener(new SearchListDocumentListener());
return panel;
}
protected void moveListUpDown(boolean isUp) {
int index = jList.getSelectedIndex();
if (isUp) {
if (index > 0) {
jList.setSelectedIndex(index - 1);
}
}
else if (index < model.getSize() - 1) {
jList.setSelectedIndex(index + 1);
}
}
private void modelChanged() {
categoryWidth = computeCategoryWidth();
setInitialSelection();
}
private int computeCategoryWidth() {
int width = 0;
List<String> categories = model.getCategories();
Font font = jList.getFont();
FontMetrics metrics = jList.getFontMetrics(font);
for (String category : categories) {
width = Math.max(width, metrics.stringWidth(category));
}
return width + 10;
}
private void chooseItem() {
SearchListEntry<T> selectedValue = jList.getSelectedValue();
if (selectedValue != null) {
chosenItemCallback.accept(selectedValue.value(), selectedValue.category());
}
}
private void filterTextChanged() {
String newFilterText = textField.getText().trim();
if (!newFilterText.equals(currentFilterText)) {
currentFilterText = newFilterText;
model.setFilter(createFilter(currentFilterText));
}
}
protected BiPredicate<T, String> createFilter(String text) {
return new DefaultFilter(text);
}
JTextField getTextField() {
return textField;
}
private class SearchListRenderer implements ListCellRenderer<SearchListEntry<T>> {
private JPanel panel;
private JLabel categoryLabel;
private Border normalBorder;
private Border lastEntryBorder;
private JSeparator jSeparator;
SearchListRenderer() {
categoryLabel = new JLabel();
panel = new JPanel(new BorderLayout());
jSeparator = new JSeparator();
int separatorHeight = jSeparator.getPreferredSize().height;
normalBorder = BorderFactory.createEmptyBorder(1, 5, separatorHeight, 5);
lastEntryBorder = BorderFactory.createEmptyBorder(1, 5, 0, 5);
}
@Override
public Component getListCellRendererComponent(JList<? extends SearchListEntry<T>> list,
SearchListEntry<T> value, int index, boolean isSelected, boolean cellHasFocus) {
panel.removeAll();
categoryLabel.setText("");
panel.setBorder(normalBorder);
// only display the category for the first entry in that category
if (value.isFirst()) {
categoryLabel.setText(value.category());
}
// Display a separator at the bottom of the last entry in the category to make
// category boundaries
if (value.isLast()) {
panel.setBorder(lastEntryBorder);
panel.add(jSeparator, BorderLayout.SOUTH);
}
Dimension size = categoryLabel.getPreferredSize();
categoryLabel.setPreferredSize(new Dimension(categoryWidth, size.height));
Component itemRendererComp =
itemRenderer.getListCellRendererComponent(list, value, index,
isSelected, false);
Color background = itemRendererComp.getBackground();
panel.add(categoryLabel, BorderLayout.WEST);
panel.add(itemRendererComp, BorderLayout.CENTER);
panel.setBackground(background);
categoryLabel.setOpaque(true);
categoryLabel.setBackground(background);
categoryLabel.setForeground(itemRendererComp.getForeground());
return panel;
}
}
private class DefaultItemRenderer extends GListCellRenderer<SearchListEntry<T>> {
@Override
public Component getListCellRendererComponent(JList<? extends SearchListEntry<T>> list,
SearchListEntry<T> value, int index,
boolean isSelected, boolean hasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index,
isSelected, false);
SearchListEntry<T> entry = value;
T t = entry.value();
label.setText(t.toString());
return label;
}
}
private class SearchListDataListener implements ListDataListener {
@Override
public void intervalAdded(ListDataEvent e) {
modelChanged();
}
@Override
public void intervalRemoved(ListDataEvent e) {
modelChanged();
}
@Override
public void contentsChanged(ListDataEvent e) {
modelChanged();
}
}
private class TextFieldKeyListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_ENTER) {
chooseItem();
}
else if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_DOWN) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(jList, e);
}
}
}
private class ListKeyListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_ENTER) {
chooseItem();
}
else if (keyCode != KeyEvent.VK_UP && keyCode != KeyEvent.VK_DOWN) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(textField, e);
textField.requestFocus();
}
}
}
private class SearchListDocumentListener implements DocumentListener {
@Override
public void insertUpdate(DocumentEvent e) {
filterTextChanged();
}
@Override
public void removeUpdate(DocumentEvent e) {
filterTextChanged();
}
@Override
public void changedUpdate(DocumentEvent e) {
filterTextChanged();
}
}
private class DefaultFilter implements BiPredicate<T, String> {
private String filterText;
DefaultFilter(String filterText) {
this.filterText = filterText.toLowerCase();
}
@Override
public boolean test(T t, String category) {
return t.toString().toLowerCase().contains(filterText);
}
}
}

View File

@ -0,0 +1,31 @@
/* ###
* 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.widgets.searchlist;
/**
* An record to hold the list item and additional information needed to properly render the item.
* @param value the list item (T)
* @param category the category for the item
* @param isFirst true if this is the first item in the category (categories are only displayed for
* the first entry)
* @param isLast true if this is the last item in the category (a separator line is displayed
* between categories)
*
* @param <T> the type of list items
*/
public record SearchListEntry<T>(T value, String category, boolean isFirst, boolean isLast) {
}

View File

@ -0,0 +1,49 @@
/* ###
* 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.widgets.searchlist;
import java.util.List;
import java.util.function.BiPredicate;
import javax.swing.ListModel;
/**
* Interface for the model for {@link SearchList}. It is an extension of a JList's model to add
* the ability to group items into categories.
*
* @param <T> the type of data items in the search list
*/
public interface SearchListModel<T> extends ListModel<SearchListEntry<T>> {
/**
* Returns the list of categories in the order they were added to the model
* @return the list of categories in the order they were added to the model
*/
public List<String> getCategories();
/**
* Sets the filter for the model data to display.
* @param filter the BiPredicate for the model data to display which will filter based on
* the item and its category
*/
public void setFilter(BiPredicate<T, String> filter);
/**
* Clean up any resources held by the model
*/
public void dispose();
}

View File

@ -0,0 +1,391 @@
/* ###
* 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.actions.dialog;
import static org.junit.Assert.*;
import java.util.*;
import java.util.stream.Collectors;
import org.junit.Test;
import docking.ActionContext;
import docking.DefaultActionContext;
import docking.action.DockingActionIf;
import docking.action.builder.ActionBuilder;
import docking.test.AbstractDockingTest;
import docking.widgets.searchlist.SearchListEntry;
import resources.Icons;
public class ActionsDialogTest extends AbstractDockingTest {
private static final boolean ENABLED = true;
private static final boolean ADD_TO_POPUP = true;
private List<String> triggeredActions = new ArrayList<>();
private Set<DockingActionIf> localActions = new HashSet<>();
private Set<DockingActionIf> globalActions = new HashSet<>();
private TestContextA contextA = new TestContextA();
private TestContextB contextB = new TestContextB();
@Test
public void testToolBarActionsForEachDisplayLevel() {
addLocal(toolbar("A", ENABLED, contextA));
addLocal(toolbar("B", !ENABLED, contextA));
addLocal(toolbar("C", ENABLED, contextB));
addLocal(toolbar("D", !ENABLED, contextB));
addGlobal(toolbar("W", ENABLED, contextA));
addGlobal(toolbar("X", !ENABLED, contextA));
addGlobal(toolbar("Y", ENABLED, contextB));
addGlobal(toolbar("Z", !ENABLED, contextB));
// level 1 includes all local toolbar actions with a valid context
ActionsModel model = buildModel(contextA, ActionDisplayLevel.LOCAL);
assertEquals(2, model.getSize());
assertModelContains(model, "A", "B");
// level 2 includes all local and global toolbar actions with a valid context
model.setDisplayLevel(ActionDisplayLevel.GLOBAL);
assertEquals(4, model.getSize());
assertModelContains(model, "A", "B", "W", "X");
// level 3 includes all local and global toolbar actions, regardless of context
model.setDisplayLevel(ActionDisplayLevel.ALL);
assertEquals(8, model.getSize());
assertModelContains(model, "A", "B", "C", "D", "W", "X", "Y", "Z");
}
@Test
public void testMenuActionsForEachDisplayLevel() {
addLocal(menuItem("A", ENABLED, contextA));
addLocal(menuItem("B", !ENABLED, contextA));
addLocal(menuItem("C", ENABLED, contextB));
addLocal(menuItem("D", !ENABLED, contextB));
addGlobal(menuItem("W", ENABLED, contextA));
addGlobal(menuItem("X", !ENABLED, contextA));
addGlobal(menuItem("Y", ENABLED, contextB));
addGlobal(menuItem("Z", !ENABLED, contextB));
// level 1 includes all local menu actions with a valid context
ActionsModel model = buildModel(contextA, ActionDisplayLevel.LOCAL);
assertEquals(2, model.getSize());
assertModelContains(model, "A", "B");
// level 2 includes all local and global menu actions with a valid context
model.setDisplayLevel(ActionDisplayLevel.GLOBAL);
assertEquals(4, model.getSize());
assertModelContains(model, "A", "B", "W", "X");
// level 3 includes all local and global menu actions
model.setDisplayLevel(ActionDisplayLevel.ALL);
assertEquals(8, model.getSize());
assertModelContains(model, "A", "B", "C", "D", "W", "X", "Y", "Z");
}
@Test
public void testKeyActionsForAllDisplayLevels() {
addLocal(keyAction("A", ENABLED, contextA));
addLocal(keyAction("B", !ENABLED, contextA));
addLocal(keyAction("C", ENABLED, contextB));
addLocal(keyAction("D", !ENABLED, contextB));
addGlobal(keyAction("W", ENABLED, contextA));
addGlobal(keyAction("X", !ENABLED, contextA));
addGlobal(keyAction("Y", ENABLED, contextB));
addGlobal(keyAction("Z", !ENABLED, contextB));
// level 1 includes all local and global keybinding actions that are valid and enabled
ActionsModel model = buildModel(contextA, ActionDisplayLevel.LOCAL);
assertEquals(2, model.getSize());
assertModelContains(model, "A", "W");
// level 2 includes all local and global keybinding actions that are valid and enabled
model.setDisplayLevel(ActionDisplayLevel.GLOBAL);
assertEquals(2, model.getSize());
assertModelContains(model, "A", "W");
// level 3 includes all local and global keybinding
model.setDisplayLevel(ActionDisplayLevel.ALL);
assertEquals(8, model.getSize());
assertModelContains(model, "A", "W");
assertModelContains(model, "A", "B", "C", "D", "W", "X", "Y", "Z");
}
@Test
public void testPopupActionsDisplayLOCAL() {
addLocal(popup("A", ENABLED, ADD_TO_POPUP, contextA));
addLocal(popup("B", ENABLED, !ADD_TO_POPUP, contextA));
addLocal(popup("C", !ENABLED, ADD_TO_POPUP, contextA));
addLocal(popup("D", !ENABLED, !ADD_TO_POPUP, contextA));
addLocal(popup("E", ENABLED, ADD_TO_POPUP, contextB));
addLocal(popup("F", ENABLED, !ADD_TO_POPUP, contextB));
addLocal(popup("G", !ENABLED, ADD_TO_POPUP, contextB));
addLocal(popup("H", !ENABLED, !ADD_TO_POPUP, contextB));
addGlobal(popup("S", ENABLED, ADD_TO_POPUP, contextA));
addGlobal(popup("T", ENABLED, !ADD_TO_POPUP, contextA));
addGlobal(popup("U", !ENABLED, ADD_TO_POPUP, contextA));
addGlobal(popup("V", !ENABLED, !ADD_TO_POPUP, contextA));
addGlobal(popup("W", ENABLED, ADD_TO_POPUP, contextB));
addGlobal(popup("X", ENABLED, !ADD_TO_POPUP, contextB));
addGlobal(popup("Y", !ENABLED, ADD_TO_POPUP, contextB));
addGlobal(popup("Z", !ENABLED, !ADD_TO_POPUP, contextB));
// display level 1 includes all local and global popup actions that are valid and addToPopup
ActionsModel model = buildModel(contextA, ActionDisplayLevel.LOCAL);
assertEquals(4, model.getSize());
assertModelContains(model, "A", "C", "S", "U");
// display level 2 includes all local and global popup actions that are valid and addToPopup
model.setDisplayLevel(ActionDisplayLevel.GLOBAL);
assertEquals(4, model.getSize());
assertModelContains(model, "A", "C", "S", "U");
// display level 3 includes all local and global popup actions
model.setDisplayLevel(ActionDisplayLevel.ALL);
assertEquals(16, model.getSize());
assertModelContains(model, "A", "B", "C", "D", "E", "F", "G", "H", "S", "T", "U", "V", "W",
"X", "Y", "Z");
}
@Test
public void testActionsOrganization() {
addLocal(toolbar("A", ENABLED, contextA));
addLocal(menuItem("B", ENABLED, contextA));
addLocal(popup("C", ENABLED, ADD_TO_POPUP, contextA));
addLocal(keyAction("D", ENABLED, contextA));
addGlobal(toolbar("W", ENABLED, contextA));
addGlobal(menuItem("X", ENABLED, contextA));
addGlobal(popup("Y", ENABLED, ADD_TO_POPUP, contextA));
addGlobal(keyAction("Z", ENABLED, contextA));
ActionsModel model = buildModel(contextA, ActionDisplayLevel.ALL);
List<String> categories = model.getCategories();
assertEquals(6, categories.size());
assertEquals(Arrays.asList("A"), getActionsForCategory(model, ActionGroup.LOCAL_TOOLBAR));
assertEquals(Arrays.asList("B"), getActionsForCategory(model, ActionGroup.LOCAL_MENU));
assertEquals(Arrays.asList("W"), getActionsForCategory(model, ActionGroup.GLOBAL_TOOLBAR));
assertEquals(Arrays.asList("X"), getActionsForCategory(model, ActionGroup.GLOBAL_MENU));
assertEquals(Arrays.asList("C", "Y"), getActionsForCategory(model, ActionGroup.POPUP));
assertEquals(Arrays.asList("D", "Z"),
getActionsForCategory(model, ActionGroup.KEYBINDING_ONLY));
}
@Test
public void testFiltering() {
addLocal(toolbar("Apple", ENABLED, contextA));
addLocal(menuItem("Banana", ENABLED, contextA));
addLocal(popup("Pear", ENABLED, ADD_TO_POPUP, contextA));
addLocal(keyAction("Kiwi", ENABLED, contextA));
ActionsModel model = buildModel(contextA, ActionDisplayLevel.ALL);
ActionChooserDialog dialog = getSwing(() -> new ActionChooserDialog(model));
assertEquals(4, model.getSize());
setFilterText(dialog, "pp");
assertEquals(1, model.getSize());
assertEquals(Arrays.asList("Apple"), getDisplayedActionNames(model));
setFilterText(dialog, "");
assertEquals(4, model.getSize());
setFilterText(dialog, "a");
assertEquals(3, model.getSize());
assertEquals(Arrays.asList("Apple", "Banana", "Pear"), getDisplayedActionNames(model));
}
@Test
public void testApplyFilterChangeDisplayLevel() {
addLocal(popup("APPLE ENABLED", ENABLED, ADD_TO_POPUP, contextA));
addLocal(popup("BANANA ENABLED", ENABLED, ADD_TO_POPUP, contextA));
addLocal(popup("APPLE DISABLED", !ENABLED, !ADD_TO_POPUP, contextA));
addLocal(popup("BANANA DISABLED", !ENABLED, !ADD_TO_POPUP, contextA));
ActionsModel model = buildModel(contextA, ActionDisplayLevel.LOCAL);
ActionChooserDialog dialog = getSwing(() -> new ActionChooserDialog(model));
assertEquals(2, model.getSize());
setFilterText(dialog, "APPLE");
assertEquals(1, model.getSize());
assertEquals(Arrays.asList("APPLE ENABLED"), getDisplayedActionNames(model));
model.setDisplayLevel(ActionDisplayLevel.ALL);
assertEquals(2, model.getSize());
assertEquals(Arrays.asList("APPLE DISABLED", "APPLE ENABLED"),
getDisplayedActionNames(model));
model.setDisplayLevel(ActionDisplayLevel.LOCAL);
assertEquals(1, model.getSize());
assertEquals(Arrays.asList("APPLE ENABLED"), getDisplayedActionNames(model));
setFilterText(dialog, "");
assertEquals(2, model.getSize());
assertEquals(Arrays.asList("APPLE ENABLED", "BANANA ENABLED"),
getDisplayedActionNames(model));
}
@Test
public void testFilteringOnKeybinding() {
addLocal(toolbar("Apple", ENABLED, contextA));
addLocal(menuItem("Banana", ENABLED, contextA));
addLocal(popup("Pear", ENABLED, ADD_TO_POPUP, contextA));
addLocal(keyAction("Kiwi", ENABLED, contextA));
ActionsModel model = buildModel(contextA, ActionDisplayLevel.ALL);
ActionChooserDialog dialog = getSwing(() -> new ActionChooserDialog(model));
assertEquals(4, model.getSize());
setFilterText(dialog, "Ctrl-1");
assertEquals(1, model.getSize());
assertEquals(Arrays.asList("Kiwi"), getDisplayedActionNames(model));
setFilterText(dialog, "");
assertEquals(4, model.getSize());
}
@Test
public void testActivatingAction() {
addLocal(toolbar("Apple", ENABLED, contextA));
addLocal(menuItem("Banana", ENABLED, contextA));
addLocal(popup("Pear", ENABLED, ADD_TO_POPUP, contextA));
addLocal(keyAction("Kiwi", ENABLED, contextA));
ActionsModel model = buildModel(contextA, ActionDisplayLevel.ALL);
ActionChooserDialog dialog = getSwing(() -> new ActionChooserDialog(model));
assertEquals(0, triggeredActions.size());
pressReturn(dialog);
assertEquals(1, triggeredActions.size());
assertEquals("Apple", triggeredActions.get(0));
}
private void pressReturn(ActionChooserDialog dialog) {
runSwing(() -> dialog.okCallback());
// simulate focus changed callback
runSwing(() -> dialog.getActionRunner().propertyChange(null));
waitForSwing();
}
private List<String> getDisplayedActionNames(ActionsModel model) {
List<String> names = new ArrayList<>();
List<SearchListEntry<DockingActionIf>> allItems = model.getDisplayedItems();
for (SearchListEntry<DockingActionIf> entry : allItems) {
names.add(entry.value().getName());
}
return names;
}
private void setFilterText(ActionChooserDialog dialog, String string) {
runSwing(() -> dialog.setFilterText(string));
}
private List<String> getActionsForCategory(ActionsModel model, ActionGroup group) {
List<String> names = new ArrayList<>();
List<SearchListEntry<DockingActionIf>> allItems = model.getAllItems();
for (SearchListEntry<DockingActionIf> entry : allItems) {
if (entry.category().equals(group.getDisplayName())) {
names.add(entry.value().getName());
}
}
return names;
}
private void assertModelContains(ActionsModel model, String... actionNames) {
List<SearchListEntry<DockingActionIf>> items = model.getDisplayedItems();
Set<String> displayedActionNames =
items.stream().map(x -> x.value().getName()).collect(Collectors.toSet());
for (String actionName : actionNames) {
if (!displayedActionNames.contains(actionName)) {
fail("Displayed actions don't contain action \"" + actionName + "\"");
}
}
}
private ActionsModel buildModel(TestContextA context, ActionDisplayLevel displayLevel) {
ActionsModel actionsModel = new ActionsModel(localActions, globalActions, context);
actionsModel.setDisplayLevel(displayLevel);
return actionsModel;
}
private void addLocal(DockingActionIf action) {
localActions.add(action);
}
private void addGlobal(DockingActionIf action) {
globalActions.add(action);
}
private DockingActionIf menuItem(String name, boolean enabled, ActionContext context) {
return new ActionBuilder(name, "Test")
.menuPath("menu", name)
.withContext(context.getClass())
.enabledWhen(c -> enabled)
.onAction(e -> triggeredActions.add(name))
.build();
}
private DockingActionIf popup(String name, boolean enabled, boolean addToPopup,
ActionContext context) {
return new ActionBuilder(name, "Test")
.popupMenuPath(name)
.withContext(context.getClass())
.enabledWhen(c -> enabled)
.popupWhen(c -> addToPopup)
.onAction(e -> triggeredActions.add(name))
.build();
}
private DockingActionIf keyAction(String name, boolean enabled, ActionContext context) {
return new ActionBuilder(name, "Test")
.keyBinding("CTRL 1")
.withContext(context.getClass())
.enabledWhen(c -> enabled)
.onAction(e -> triggeredActions.add(name))
.build();
}
private DockingActionIf toolbar(String name, boolean enabled, ActionContext context) {
return new ActionBuilder(name, "Test")
.toolBarIcon(Icons.ADD_ICON)
.withContext(context.getClass())
.enabledWhen(c -> enabled)
.onAction(e -> triggeredActions.add(name))
.build();
}
private class TestContextA extends DefaultActionContext {
// just need different context class
}
private class TestContextB extends DefaultActionContext {
// just need different context class
}
}

View File

@ -0,0 +1,123 @@
/* ###
* 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.widgets.searchlist;
import static org.junit.Assert.*;
import java.awt.BorderLayout;
import java.awt.event.KeyEvent;
import java.util.List;
import javax.swing.*;
import org.junit.Before;
import org.junit.Test;
import docking.test.AbstractDockingTest;
public class SearchListTest extends AbstractDockingTest {
private SearchList<String> searchList;
private DefaultSearchListModel<String> model;
private JFrame parentFrame;
private String lastChoiceValue;
private String lastChoiceCategory;
@Before
public void setUp() throws Exception {
model = createModel();
searchList = new SearchList<>(model, (t, c) -> choiceMade(t, c));
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.add(searchList, BorderLayout.CENTER);
parentFrame = new JFrame(SearchList.class.getName());
parentFrame.getContentPane().removeAll();
parentFrame.getContentPane().add(panel);
parentFrame.pack();
parentFrame.setVisible(true);
}
@Test
public void testFilterReducesNumberOfElements() {
assertEquals(8, model.getSize());
List<SearchListEntry<String>> allItems = model.getAllItems();
List<SearchListEntry<String>> displayed = model.getDisplayedItems();
assertEquals(allItems, displayed);
setFilterText("d");
assertEquals(2, model.getSize());
List<SearchListEntry<String>> displayedItems = model.getDisplayedItems();
assertEquals("date", displayedItems.get(0).value());
assertEquals("dill", displayedItems.get(1).value());
}
@Test
public void testCategoryNotConsideredInFilter() {
assertEquals(8, model.getSize());
setFilterText("f");
assertEquals(0, model.getSize());
}
@Test
public void testClearFilterRestores() {
assertEquals(8, model.getSize());
setFilterText("apple");
assertEquals(1, model.getSize());
setFilterText("");
assertEquals(8, model.getSize());
}
@Test
public void testSelect() {
JTextField textField = searchList.getTextField();
assertNull(lastChoiceValue);
assertNull(lastChoiceCategory);
triggerEnter(textField);
assertEquals("apple", lastChoiceValue);
assertEquals("fruits", lastChoiceCategory);
triggerActionKey(textField, 0, KeyEvent.VK_DOWN);
triggerEnter(textField);
assertEquals("banana", lastChoiceValue);
assertEquals("fruits", lastChoiceCategory);
}
private DefaultSearchListModel<String> createModel() {
DefaultSearchListModel<String> listModel = new DefaultSearchListModel<>();
List<String> fruits = List.of("apple", "banana", "cherry", "date");
listModel.add("fruits", fruits);
List<String> veggies =
List.of("artichoke", "beet", "cabbage", "dill");
listModel.add("vegetables", veggies);
return listModel;
}
private void choiceMade(String value, String category) {
lastChoiceValue = value;
lastChoiceCategory = category;
}
private void setFilterText(String text) {
runSwing(() -> searchList.setFilterText(text));
}
}

View File

@ -20,6 +20,8 @@ import static java.awt.event.KeyEvent.*;
import static javax.swing.KeyStroke.*;
import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import javax.swing.KeyStroke;
@ -52,6 +54,9 @@ public class SystemKeyBindings {
public static final KeyStroke COMPONENT_THEME_INFO_KEY = getKeyStroke(VK_F9, CTRL_ALT_SHIFT);
public static final KeyStroke ACTION_CHOOSER_KEY =
KeyStroke.getKeyStroke(KeyEvent.VK_3, InputEvent.CTRL_DOWN_MASK);
private SystemKeyBindings() {
// utils class
}

View File

@ -26,6 +26,7 @@ import javax.swing.text.*;
import generic.text.TextLayoutGraphics;
import generic.theme.GAttributes;
import generic.theme.GColor;
import ghidra.util.html.HtmlLineSplitter;
import utilities.util.reflection.ReflectionUtilities;
@ -919,6 +920,9 @@ public class HTMLUtilities {
* @return a string of the format #RRGGBB.
*/
public static String toHexString(Color color) {
if (color instanceof GColor gColor) {
return gColor.toHexString();
}
// this will format a color value as a 6 digit hex string (e.g. #rrggbb)
return String.format("#%06X", color.getRGB() & 0xffffff);
}

View File

@ -59,6 +59,11 @@ public class DummyToolActions implements DockingToolActions {
return null;
}
@Override
public Set<DockingActionIf> getGlobalActions() {
return null;
}
@Override
public void removeLocalAction(ComponentProvider provider, DockingActionIf action) {
// stub
@ -73,4 +78,9 @@ public class DummyToolActions implements DockingToolActions {
public void registerSharedActionPlaceholder(SharedDockingActionPlaceholder placeholder) {
// stub
}
@Override
public Set<DockingActionIf> getLocalActions(ComponentProvider provider) {
return null;
}
}

View File

@ -81,6 +81,25 @@ public class Dummy {
};
}
/**
* Creates a dummy {@link Predicate} that always returns true.
* @param <T> the type of the value being tested
* @return the predicate that always returns true
*/
public static <T> Predicate<T> predicate() {
return t -> true;
}
/**
* Creates a dummy {@link BiPredicate} that always returns true.
* @param <T> the type of the first argument to the predicate
* @param <U> the type of the second argument the predicate
* @return the BiPredicate that always returns true
*/
public static <T, U> BiPredicate<T, U> biPredicate() {
return (t, u) -> true;
}
/**
* 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}.
@ -148,4 +167,29 @@ public class Dummy {
public static Runnable ifNull(Runnable r) {
return r == null ? runnable() : r;
}
/**
* Returns the given Predicate object if it is not {@code null}. Otherwise, a
* {@link #predicate()} (which always returns true) is returned. This is useful to avoid
* using {@code null}.
*
* @param p the predicate function to check for {@code null}
* @return a non-null predicate
*/
public static <T> Predicate<T> ifNull(Predicate<T> p) {
return p == null ? predicate() : p;
}
/**
* Returns the given BiPredicate object if it is not {@code null}. Otherwise, a
* {@link #biPredicate()} (which always returns true) is returned. This is useful to avoid
* using {@code null}.
*
* @param p the predicate function to check for {@code null}
* @return a non-null predicate
*/
public static <T, U> BiPredicate<T, U> ifNull(BiPredicate<T, U> p) {
return p == null ? biPredicate() : p;
}
}

View File

@ -0,0 +1,39 @@
/* ###
* 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 help.screenshot;
import org.junit.Test;
import docking.actions.dialog.ActionChooserDialog;
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
public class KeyboardNavigationScreenShots extends GhidraScreenShotGenerator {
@Test
public void testActionsDialog() {
CodeViewerProvider provider = getProvider(CodeViewerProvider.class);
ActionChooserDialog dialog = getSwing(() -> {
ActionChooserDialog actionsDialog =
new ActionChooserDialog(tool, provider, provider.getActionContext(null));
actionsDialog.setPreferredSize(600, 400);
return actionsDialog;
});
runSwing(() -> tool.showDialog(dialog), false);
captureDialog();
close(dialog);
}
}