mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-22 12:11:55 +00:00
GP-4267 Quick Action Dialog
This commit is contained in:
parent
21f1a63f51
commit
a88106460b
@ -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|
|
||||
|
@ -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" />
|
||||
|
@ -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 <CTRL> TAB will transfer focus to the next component. And
|
||||
<SHIFT> TAB and <CTRL><SHIFT>TAB will transfer focus to the previous component
|
||||
in the cycle. TAB and <SHIFT>TAB do not always work as they are sometimes used by
|
||||
individual components such as text components, but the <CTRL> versions should work
|
||||
universally.</P>
|
||||
<P>
|
||||
</P>Ghidra also provides some handy shortcut keys for navigation:
|
||||
<UL>
|
||||
<LI><CTRL>F3 - Transfer focus to the next window or dialog.
|
||||
<LI><CTRL><SHIFT>F3 - Transfer focus to the previous window or dialog.
|
||||
<LI><CTRL>J - Transfer focus (Jump) to the next component provider (titled component).
|
||||
<LI><CTRL><SHIFT>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 <ALT>F to access
|
||||
the File menu.</P>
|
||||
<P>
|
||||
Context menus can be invoked using the <SHIFT>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>
|
@ -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);
|
||||
|
@ -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|
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -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 <CTRL> TAB will transfer focus to
|
||||
the next component. And <SHIFT> TAB and <CTRL><SHIFT>TAB will transfer
|
||||
focus to the previous component in the cycle. TAB and <SHIFT>TAB do not always work
|
||||
as they are sometimes used by individual components such as text components, but the
|
||||
<CTRL> versions should work universally.</P>
|
||||
|
||||
<P>Ghidra also provides some handy shortcut keys for navigation:</P>
|
||||
|
||||
<UL>
|
||||
<LI><CTRL>F3 - Transfer focus to the next window or dialog.</LI>
|
||||
|
||||
<LI><CTRL><SHIFT>F3 - Transfer focus to the previous window or dialog.</LI>
|
||||
|
||||
<LI><CTRL>J - Transfer focus (Jump) to the next component provider (titled
|
||||
component).</LI>
|
||||
|
||||
<LI><CTRL><SHIFT>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 <ALT>F to
|
||||
access the File menu.</P>
|
||||
|
||||
<P>Context menus can be invoked using the <SHIFT>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 <CTRL> 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
|
||||
<ENTER> 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 <CTRL> 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
|
||||
(<CTRL> 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 |
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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() {
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
@ -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();
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user