GP-1619 - Fixed bug that caused some MultiStateDockinActions to get called twice when clicked

This commit is contained in:
dragonmacher 2022-01-05 17:03:56 -05:00
parent 436bb4873e
commit 6934c33aa6
25 changed files with 197 additions and 627 deletions

View file

@ -31,10 +31,6 @@ public class MultiActionBuilder
* List of actions for the the MultActionDockingAction
*/
private List<DockingActionIf> actionList = Collections.emptyList();
/**
* determines if the the main action is invokable
*/
private boolean performActionOnButtonClick = true;
/**
* Builder constructor
@ -61,7 +57,6 @@ public class MultiActionBuilder
};
decorateAction(action);
action.setActions(actionList);
action.setPerformActionOnButtonClick(performActionOnButtonClick);
return action;
}
@ -81,26 +76,8 @@ public class MultiActionBuilder
return self();
}
/**
* Configure whether to perform actions on a button click.
* See {@link MultiActionDockingAction#setPerformActionOnButtonClick(boolean)}
*
* @param b true if the main action is invokable
* @return this MultiActionDockingActionBuilder (for chaining)
*/
public MultiActionBuilder performActionOnButtonClick(boolean b) {
this.performActionOnButtonClick = b;
return self();
}
@Override
protected void validate() {
// if the MultiAction performs an action when the main button is presseed, make sure that
// an action callback has been defined in before building (which is what super validate
// does). Otherwise, don't force the client to define an action callback if it won't be used.
if (performActionOnButtonClick) {
super.validate();
}
if (actionList == null) {
throw new IllegalStateException("No ActionList has been set");
}

View file

@ -22,7 +22,8 @@ import java.util.function.BiConsumer;
import javax.swing.Icon;
import docking.ActionContext;
import docking.menu.*;
import docking.menu.ActionState;
import docking.menu.MultiStateDockingAction;
import docking.widgets.EventTrigger;
/**
@ -35,7 +36,6 @@ public class MultiStateActionBuilder<T> extends
private BiConsumer<ActionState<T>, EventTrigger> actionStateChangedCallback;
private boolean useCheckboxForIcons;
private boolean performActionOnButtonClick = false;
private List<ActionState<T>> states = new ArrayList<>();
@ -68,18 +68,6 @@ public class MultiStateActionBuilder<T> extends
return self();
}
/**
* Configure whether to perform actions on a button click.
* See {@link MultiActionDockingAction#setPerformActionOnButtonClick(boolean)}
*
* @param b true if the main action is invokable
* @return this MultiActionDockingActionBuilder (for chaining)
*/
public MultiStateActionBuilder<T> performActionOnButtonClick(boolean b) {
this.performActionOnButtonClick = b;
return self();
}
/**
* Overrides the default icons for actions shown in popup menu of the multi-state action. By
* default, the popup menu items will use the icons as provided by the {@link ActionState}.
@ -103,7 +91,7 @@ public class MultiStateActionBuilder<T> extends
* @return this MultiActionDockingActionBuilder (for chaining)
*/
public MultiStateActionBuilder<T> addState(String displayName, Icon icon, T userData) {
states.add(new ActionState<T>(displayName, icon, userData));
states.add(new ActionState<>(displayName, icon, userData));
return self();
}
@ -133,7 +121,7 @@ public class MultiStateActionBuilder<T> extends
public MultiStateDockingAction<T> build() {
validate();
MultiStateDockingAction<T> action =
new MultiStateDockingAction<>(name, owner, isToolbarAction()) {
new MultiStateDockingAction<>(name, owner) {
@Override
public void actionStateChanged(ActionState<T> newActionState,
@ -142,10 +130,13 @@ public class MultiStateActionBuilder<T> extends
}
@Override
protected void doActionPerformed(ActionContext context) {
public void actionPerformed(ActionContext context) {
if (actionCallback != null) {
actionCallback.accept(context);
}
else {
super.actionPerformed(context);
}
}
};
@ -154,16 +145,12 @@ public class MultiStateActionBuilder<T> extends
}
decorateAction(action);
action.setPerformActionOnPrimaryButtonClick(performActionOnButtonClick);
action.setUseCheckboxForIcons(useCheckboxForIcons);
return action;
}
@Override
protected void validate() {
if (performActionOnButtonClick) {
super.validate(); // require an action callback has been defined
}
if (actionStateChangedCallback == null) {
throw new IllegalStateException(
"Can't build a MultiStateDockingAction without an action state changed callback");

View file

@ -22,13 +22,14 @@ import javax.swing.JButton;
import docking.ActionContext;
import docking.action.*;
import ghidra.util.Swing;
/**
* A class that supports multiple sub-actions, as well as a primary action. This is useful for
* actions that perform navigation operations.
* <p>
* Clients may add actions to this class with the intention that they will be accessible
* to the user via a GUI; for example, from a popup menu.
* Clients may add actions to this class with the intention that they will be accessible to the
* user via a GUI; for example, from a popup menu.
* <p>
* Actions added must have menu bar data set.
*
@ -36,18 +37,20 @@ import docking.action.*;
* the user to execute.
*
* <p>
* If the user executes this action directly, then
* {@link #actionPerformed(ActionContext)} will be called. Otherwise, the
* {@link DockingAction#actionPerformed(ActionContext)} method of the sub-action
* that was executed will be called.
* If the user executes this action directly (by clicking the non-popup section of the button),
* then {@link #actionPerformed(ActionContext)} will be called. By default, when the button is
* clicked, the popup menu is shown. To change this behavior, override
* {@link #actionPerformed(ActionContext)}. If an item of the popup menu is clicked, then the
* {@link DockingAction#actionPerformed(ActionContext)} method of the sub-action that was executed
* will be called.
*
* @see MultiStateDockingAction
*/
public abstract class MultiActionDockingAction extends DockingAction
public class MultiActionDockingAction extends DockingAction
implements MultiActionDockingActionIf {
private List<DockingActionIf> actionList = Collections.emptyList();
private boolean performActionOnButtonClick = true;
private MultipleActionDockingToolbarButton multipleButton;
public MultiActionDockingAction(String name, String owner) {
super(name, owner);
@ -67,24 +70,23 @@ public abstract class MultiActionDockingAction extends DockingAction
return actionList;
}
/**
* This method is called when the user clicks the button <B>when this action is used as part of
* the default {@link DockingAction} framework.</B>
*
* This is the callback to be overridden when the child wishes to respond to user button
* presses that are on the button and not the drop-down. The default behavior is to show the
* popup menu when the button is clicked.
*/
@Override
public JButton doCreateButton() {
MultipleActionDockingToolbarButton button = new MultipleActionDockingToolbarButton(this);
button.setPerformActionOnButtonClick(performActionOnButtonClick);
return button;
public void actionPerformed(ActionContext context) {
Swing.runLater(() -> multipleButton.showPopup());
}
/**
* By default a click on this action will trigger <code>actionPerformed()</code> to be called.
* You can call this method to disable that feature. When called with <code>false</code>, this
* method will effectively let the user click anywhere on the button or its drop-down arrow
* to show the popup menu. During normal operation, the user can only show the popup by
* clicking the drop-down arrow.
* @param performActionOnButtonClick if true, pressing the button calls actionPerformed;
* otherwise it pops up the menu.
*/
public void setPerformActionOnButtonClick(boolean performActionOnButtonClick) {
this.performActionOnButtonClick = performActionOnButtonClick;
@Override
public JButton doCreateButton() {
multipleButton = new MultipleActionDockingToolbarButton(this);
return multipleButton;
}
public static DockingActionIf createSeparator() {

View file

@ -15,7 +15,6 @@
*/
package docking.menu;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
@ -23,25 +22,24 @@ import javax.swing.Icon;
import javax.swing.JButton;
import docking.ActionContext;
import docking.DockingWindowManager;
import docking.action.*;
import docking.widgets.EventTrigger;
import ghidra.util.HelpLocation;
import ghidra.util.SystemUtilities;
import ghidra.util.Swing;
import ghidra.util.exception.AssertException;
import resources.icons.EmptyIcon;
/**
* An action that can be in one of multiple states. The button of this action has a
* drop-down icon that allows users to change the state of the button. Also, by default, as
* the user presses the button, it will execute the action corresponding to the current
* state.
*
* <p>Warning: if you use this action in a toolbar, then be sure to call the
* {@link #MultiStateDockingAction(String, String, boolean) correct constructor}. If you call
* another constructor, or pass false for this boolean above, your
* {@link #doActionPerformed(ActionContext)} method will get called twice.
* drop-down icon that allows users to change the state of the button. As the user changes the
* state of this action, {@link #actionStateChanged(ActionState, EventTrigger)} will be called.
* Clients may also use the button of this action to respond to button presses by overriding
* {@link #actionPerformed(ActionContext)}.
*
* <p>This action is intended primarily for use as toolbar actions. Alternatively, some clients
* use this action to add a button to custom widgets. In the custom usage case, clients should use
* {@link NonToolbarMultiStateAction}.
*
* @param <T> the type of the user data
* @see MultiActionDockingAction
*/
@ -54,16 +52,9 @@ public abstract class MultiStateDockingAction<T> extends DockingAction {
private MultiActionDockingActionIf multiActionGenerator;
private MultipleActionDockingToolbarButton multipleButton;
private boolean performActionOnPrimaryButtonClick = true;
private Icon defaultIcon;
private boolean useCheckboxForIcons;
// A listener that will get called when the button (not the popup) is clicked. Toolbar
// actions do not use this listener.
private ActionListener clickListener = e -> {
// stub for toolbar actions
};
/**
* Call this constructor with this action will not be added to a toolbar
*
@ -72,7 +63,11 @@ public abstract class MultiStateDockingAction<T> extends DockingAction {
* @see #MultiStateDockingAction(String, String, boolean)
*/
public MultiStateDockingAction(String name, String owner) {
this(name, owner, false);
super(name, owner);
multiActionGenerator = context -> getStateActions();
// set this here so we don't have to check for null elsewhere
super.setToolBarData(new ToolBarData(null));
}
/**
@ -82,50 +77,31 @@ public abstract class MultiStateDockingAction<T> extends DockingAction {
* @param name the action name
* @param owner the owner
* @param isToolbarAction true if this action is a toolbar action
* @deprecated use {@link #MultiStateDockingAction(String, String)}
*/
@Deprecated(forRemoval = true, since = "10.2")
protected MultiStateDockingAction(String name, String owner, boolean isToolbarAction) {
super(name, owner);
multiActionGenerator = context -> getStateActions();
// set this here so we don't have to check for null elsewhere
super.setToolBarData(new ToolBarData(null));
if (!isToolbarAction) {
// we need this listener to perform the action when the user click the button;
// toolbar actions have their own listener
clickListener = e -> {
actionPerformed(getActionContext());
};
}
this(name, owner);
}
/**
* This method will be called as the user changes the selected button state
* @param newActionState the newly selected state
* @param trigger the source of the event
*/
public abstract void actionStateChanged(ActionState<T> newActionState, EventTrigger trigger);
/**
* If <code>doPerformAction</code> is <code>true</code>, then, when the user clicks the
* button and not the drop-down arrow, the {@link #doActionPerformed(ActionContext)}
* method will be called. If <code>doPerformAction</code> is <code>false</code>, then, when
* the user clicks the button and not the drop-down arrow, the popup menu will be shown, just
* as if the user had clicked the drop-down arrow.
* <p>
* Also, if the parameter is true, then the button will behave like a button in terms of
* mouse feedback. If false, then the button will behave more like a label.
*
* @param doPerformAction true to call {@link #doActionPerformed(ActionContext)} when the
* user presses the button for this action (not the drop-down menu; see above)
* This method is called when the user clicks the button <B>when this action is used as part of
* the default {@link DockingAction} framework.</B>
*
* This is the callback to be overridden when the child wishes to respond to user button
* presses that are on the button and not the drop-down. The default behavior is to show the
* popup menu when the button is clicked.
*/
public void setPerformActionOnPrimaryButtonClick(boolean doPerformAction) {
performActionOnPrimaryButtonClick = doPerformAction;
if (multipleButton == null) {
return;
}
multipleButton.setPerformActionOnButtonClick(performActionOnPrimaryButtonClick);
multipleButton.removeActionListener(clickListener);
if (performActionOnPrimaryButtonClick) {
multipleButton.addActionListener(clickListener);
}
@Override
public void actionPerformed(ActionContext context) {
Swing.runLater(() -> multipleButton.showPopup());
}
/**
@ -151,38 +127,6 @@ public abstract class MultiStateDockingAction<T> extends DockingAction {
this.defaultIcon = icon;
}
@Override
public final void actionPerformed(ActionContext context) {
if (!performActionOnPrimaryButtonClick) {
SystemUtilities.runSwingLater(() -> multipleButton.showPopup(null));
return;
}
doActionPerformed(context);
}
/**
* This is the callback to be overridden when the child wishes to respond to user button
* presses that are on the button and not the drop-down. This will only be called if
* {@link #performActionOnPrimaryButtonClick} is true.
*
* @param context the action context
*/
protected void doActionPerformed(ActionContext context) {
// override me to do work
}
private ActionContext getActionContext() {
DockingWindowManager manager = DockingWindowManager.getActiveInstance();
ActionContext context = manager.getActionContext(this);
if (context == null) {
context = new ActionContext();
}
return context;
}
protected List<DockingActionIf> getStateActions() {
ActionState<T> selectedState = actionStates.get(currentStateIndex);
List<DockingActionIf> actions = new ArrayList<>(actionStates.size());
@ -295,15 +239,6 @@ public abstract class MultiStateDockingAction<T> extends DockingAction {
@Override
public JButton doCreateButton() {
multipleButton = new MultipleActionDockingToolbarButton(multiActionGenerator);
multipleButton.setPerformActionOnButtonClick(performActionOnPrimaryButtonClick);
if (performActionOnPrimaryButtonClick) {
multipleButton.addActionListener(clickListener);
}
else {
multipleButton.removeActionListener(clickListener);
}
if (currentStateIndex >= 0) {
ActionState<T> actionState = actionStates.get(currentStateIndex);
setButtonState(actionState);
@ -350,6 +285,10 @@ public abstract class MultiStateDockingAction<T> extends DockingAction {
return getName() + ": " + getCurrentState().getName();
}
protected void showPopup() {
multipleButton.showPopup();
}
//==================================================================================================
// Inner Classes
//==================================================================================================

View file

@ -43,11 +43,11 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
private static int ARROW_PADDING = 4;
private PopupMouseListener popupListener;
private JPopupMenu popupMenu;
private Shape popupContext;
private long popupLastClosedTime;
private final MultiActionDockingActionIf multipleAction;
private boolean iconBorderEnabled = true;
private boolean entireButtonShowsPopupMenu;
public MultipleActionDockingToolbarButton(MultiActionDockingActionIf action) {
multipleAction = action;
@ -74,21 +74,6 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
return disabledIcon;
}
/**
* By default a click on this button will trigger <code>actionPerformed()</code> to be called.
* You can call this method to disable that feature. When called with <code>false</code>, this
* method will effectively let the user click anywhere on the button or its drop-down arrow
* to show the popup menu. During normal operation, the user can only show the popup by
* clicking the drop-down arrow.
*
* @param performActionOnButtonClick true to perform the action when the button is clicked
*/
public void setPerformActionOnButtonClick(boolean performActionOnButtonClick) {
entireButtonShowsPopupMenu = !performActionOnButtonClick;
iconBorderEnabled = performActionOnButtonClick;
popupContext = createPopupContext();
}
@Override
protected void paintBorder(Graphics g) {
Border buttonBorder = getBorder();
@ -98,10 +83,7 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
Insets borderInsets = buttonBorder.getBorderInsets(this);
int leftIconWidth = primaryIcon.getIconWidth() + (borderInsets.left + borderInsets.right);
if (iconBorderEnabled) {
buttonBorder.paintBorder(this, g, 0, 0, leftIconWidth, getHeight());
}
buttonBorder.paintBorder(this, g, 0, 0, leftIconWidth, getHeight());
int rightButtonWidth =
ARROW_WIDTH + ARROW_PADDING + (borderInsets.left + borderInsets.right);
buttonBorder.paintBorder(this, g, leftIconWidth, 0, rightButtonWidth, getHeight());
@ -132,10 +114,6 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
}
private Shape createPopupContext() {
if (entireButtonShowsPopupMenu) {
return new Rectangle(0, 0, getWidth(), getHeight());
}
Border buttonBorder = getBorder();
Insets borderInsets =
buttonBorder == null ? new Insets(0, 0, 0, 0) : buttonBorder.getBorderInsets(this);
@ -163,10 +141,31 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
/**
* Show a popup containing all the actions below the button
* @param listener for the created popup menu
* @return the popup menu that was shown
*/
JPopupMenu showPopup(PopupMenuListener listener) {
JPopupMenu showPopup() {
if (popupIsShowing()) {
popupMenu.setVisible(false);
return null;
}
//
// showPopup() will handled 2 cases when this action's button is clicked:
// 1) show a popup if it was not showing
// 2) hide the popup if it was showing
//
// Case 2 requires timestamps. Java will close the popup as the button is clicked. This
// means that when we are told to show the popup as the result of a click, the popup will
// never be showing. To work around this, we track the elapsed time since last click. If
// the period is too short, then we assume Java closed the popup when the click happened
//and thus we should ignore it.
//
long elapsedTime = System.currentTimeMillis() - popupLastClosedTime;
if (elapsedTime < 500) { // somewhat arbitrary time window
return null;
}
JPopupMenu menu = new JPopupMenu();
List<DockingActionIf> actionList = multipleAction.getActionList(getActionContext());
for (DockingActionIf dockingAction : actionList) {
@ -202,9 +201,7 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
menu.add(item);
}
if (listener != null) {
menu.addPopupMenuListener(listener);
}
menu.addPopupMenuListener(popupListener);
Point p = getPopupPoint();
menu.show(this, p.x, p.y);
return menu;
@ -215,6 +212,10 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
return new Point(0, bounds.y + bounds.height);
}
private boolean popupIsShowing() {
return (popupMenu != null) && popupMenu.isVisible();
}
//==================================================================================================
// Inner Classes
//==================================================================================================
@ -285,8 +286,6 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
private class PopupMouseListener extends MouseAdapter implements PopupMenuListener {
private final MouseListener[] parentListeners;
private JPopupMenu popupMenu;
private long actionID = 0; // used to determine when the popup was closed by clicking the button
public PopupMouseListener(MouseListener[] parentListeners) {
this.parentListeners = parentListeners;
@ -294,16 +293,6 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
@Override
public void mousePressed(MouseEvent e) {
// close the popup if the user clicks the button while the popup is visible
if (popupIsShowing() && e.getClickCount() == 1) { // ignore double-click when the menu is up
popupMenu.setVisible(false);
return;
}
long eventTime = System.currentTimeMillis();
if (actionID == eventTime) {
return;
}
Point clickPoint = e.getPoint();
if (isEnabled() && popupContext.contains(clickPoint)) {
@ -311,8 +300,8 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
// Unusual Code Alert: we need to put this call in an invoke later, since Java
// will update the focused window after we click. We need the focus to be
// correct before we show, since our menu is built with actions based upon the
// focused dude.
Swing.runLater(() -> popupMenu = showPopup(PopupMouseListener.this));
// focused component.
Swing.runLater(() -> popupMenu = showPopup());
e.consume();
model.setPressed(false);
@ -372,10 +361,6 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
}
}
private boolean popupIsShowing() {
return (popupMenu != null) && popupMenu.isVisible();
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
// no-op
@ -383,7 +368,7 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
actionID = System.currentTimeMillis(); // hacktastic!
popupLastClosedTime = System.currentTimeMillis();
}
@Override

View file

@ -15,7 +15,13 @@
*/
package docking.menu;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import docking.action.DockingAction;
import docking.widgets.EventTrigger;
import ghidra.util.Swing;
/**
* A class for clients that wish to create a button that has multiple states, controlled by a
@ -27,12 +33,39 @@ import docking.widgets.EventTrigger;
* {@link #actionStateChanged(ActionState, EventTrigger)} callback. Call
* {@link #createButton()} and add the return value to your UI.
*
* @param <T>
* @param <T> the type
* @see MultiStateDockingAction
*/
public abstract class NonToolbarMultiStateAction<T> extends MultiStateDockingAction<T> {
// A listener that will get called when the button (not the popup) is clicked. Toolbar
// actions do not need this functionality, since the toolbar API will call actionPerfomred().
private ActionListener clickListener = e -> {
actionPerformed();
};
public NonToolbarMultiStateAction(String name, String owner) {
super(name, owner);
}
@Override
public JButton doCreateButton() {
JButton button = super.doCreateButton();
button.addActionListener(clickListener);
return button;
}
/**
* This method is called when the user clicks the button <B>when this action is used as a
* custom button provider and not installed into the default {@link DockingAction} framework.
* </B>
*
* This is the callback to be overridden when the child wishes to respond to user button
* presses that are on the button and not the drop-down. The default behavior is to show the
* popup menu when the button is clicked.
*/
protected void actionPerformed() {
Swing.runLater(() -> showPopup());
}
}

View file

@ -28,7 +28,6 @@ import javax.swing.table.TableModel;
import org.jdom.Element;
import docking.ActionContext;
import docking.DockingWindowManager;
import docking.help.HelpService;
import docking.menu.*;
@ -413,12 +412,12 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
}
@Override
protected void doActionPerformed(ActionContext context) {
protected void actionPerformed() {
showFilterDialog(tableModel);
}
};
columnFilterAction.setPerformActionOnPrimaryButtonClick(true);
HelpLocation helpLocation = new HelpLocation("Trees", "Column_Filters");
columnFilterAction.setHelpLocation(helpLocation);