GP-1440 fixing issue global actions shared across windows getting the

wrong context
This commit is contained in:
ghidravore 2021-11-03 16:20:43 -04:00
parent b7ef0fffed
commit c625da90a4
6 changed files with 157 additions and 120 deletions

View file

@ -100,7 +100,7 @@ public class ActionToGuiMapper {
void update() { void update() {
menuAndToolBarManager.update(); menuAndToolBarManager.update();
contextChangedAll(); contextChanged();
} }
void dispose() { void dispose() {
@ -117,12 +117,8 @@ public class ActionToGuiMapper {
return menuBarMenuHandler; return menuBarMenuHandler;
} }
void contextChangedAll() { void contextChanged() {
menuAndToolBarManager.contextChangedAll(); menuAndToolBarManager.contextChanged();
}
void contextChanged(ComponentPlaceholder placeHolder) {
menuAndToolBarManager.contextChanged(placeHolder);
} }
PopupActionManager getPopupActionManager() { PopupActionManager getPopupActionManager() {

View file

@ -159,7 +159,7 @@ public class ComponentPlaceholder {
return group; return group;
} }
DetachedWindowNode getWindowNode() { DetachedWindowNode getDetachedWindowNode() {
Node node = compNode.parent; Node node = compNode.parent;
while (node != null) { while (node != null) {
if (node instanceof DetachedWindowNode) { if (node instanceof DetachedWindowNode) {
@ -170,6 +170,17 @@ public class ComponentPlaceholder {
return null; return null;
} }
WindowNode getWindowNode() {
Node node = compNode.parent;
while (node != null) {
if (node instanceof WindowNode) {
return (WindowNode) node;
}
node = node.parent;
}
return null;
}
WindowNode getTopLevelNode() { WindowNode getTopLevelNode() {
if (compNode == null) { if (compNode == null) {
return null; return null;
@ -279,9 +290,9 @@ public class ComponentPlaceholder {
// makes sure that the given window is not in an iconified state // makes sure that the given window is not in an iconified state
private void activateWindow() { private void activateWindow() {
DetachedWindowNode windowNode = getWindowNode(); DetachedWindowNode windowNode = getDetachedWindowNode();
if (windowNode != null) { if (windowNode != null) {
Window window = getWindowNode().getWindow(); Window window = getDetachedWindowNode().getWindow();
if (window instanceof Frame) { if (window instanceof Frame) {
Frame frame = (Frame) window; Frame frame = (Frame) window;
frame.setState(Frame.NORMAL); frame.setState(Frame.NORMAL);

View file

@ -630,7 +630,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
placeholder.update(); placeholder.update();
scheduleUpdate(); scheduleUpdate();
DetachedWindowNode wNode = placeholder.getWindowNode(); DetachedWindowNode wNode = placeholder.getDetachedWindowNode();
if (wNode != null) { if (wNode != null) {
wNode.updateTitle(); wNode.updateTitle();
} }
@ -2225,19 +2225,16 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
} }
public void contextChanged(ComponentProvider provider) { public void contextChanged(ComponentProvider provider) {
// if provider is specified, update its local menu and tool bar actions
if (provider == null) { if (provider != null) {
actionToGuiMapper.contextChangedAll(); // this updates the actions for all windows ComponentPlaceholder placeholder = getActivePlaceholder(provider);
return; if (placeholder != null) {
placeholder.contextChanged();
}
} }
ComponentPlaceholder placeholder = getActivePlaceholder(provider); // always update the global tool menu and tool bar actions
if (placeholder == null) { actionToGuiMapper.contextChanged();
return;
}
placeholder.contextChanged();
actionToGuiMapper.contextChanged(placeholder);
} }
/** /**
@ -2326,12 +2323,16 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
} }
void notifyContextListeners(ComponentPlaceholder placeHolder, ActionContext actionContext) { /**
* This call will notify any context listeners that the context has changed.
if (placeHolder == focusedPlaceholder) { *
for (DockingContextListener listener : contextListeners) { * <p>Our {@link #contextChanged(ComponentProvider)} method will eventually call back into this
listener.contextChanged(actionContext); * method after any buffering has taken place.
} * @param context the context
*/
void doContextChanged(ActionContext context) {
for (DockingContextListener listener : contextListeners) {
listener.contextChanged(context);
} }
} }

View file

@ -21,7 +21,12 @@ import docking.action.DockingActionIf;
import docking.menu.MenuGroupMap; import docking.menu.MenuGroupMap;
import docking.menu.MenuHandler; import docking.menu.MenuHandler;
import ghidra.util.Swing; import ghidra.util.Swing;
import ghidra.util.task.AbstractSwingUpdateManager;
import ghidra.util.task.SwingUpdateManager;
/**
* Class to manage all the global actions that show up on the main tool menubar or toolbar
*/
public class GlobalMenuAndToolBarManager implements DockingWindowListener { public class GlobalMenuAndToolBarManager implements DockingWindowListener {
private Map<WindowNode, WindowActionManager> windowToActionManagerMap; private Map<WindowNode, WindowActionManager> windowToActionManagerMap;
@ -29,6 +34,12 @@ public class GlobalMenuAndToolBarManager implements DockingWindowListener {
private final MenuGroupMap menuGroupMap; private final MenuGroupMap menuGroupMap;
private final DockingWindowManager windowManager; private final DockingWindowManager windowManager;
// set the max delay low enough so that users can't interact with out-of-date actions before
// they get updated
private SwingUpdateManager updateManager =
new SwingUpdateManager(AbstractSwingUpdateManager.DEFAULT_MIN_DELAY, 500,
"Context Update Manager", () -> updateActions());
public GlobalMenuAndToolBarManager(DockingWindowManager windowManager, MenuHandler menuHandler, public GlobalMenuAndToolBarManager(DockingWindowManager windowManager, MenuHandler menuHandler,
MenuGroupMap menuGroupMap) { MenuGroupMap menuGroupMap) {
@ -77,11 +88,15 @@ public class GlobalMenuAndToolBarManager implements DockingWindowListener {
} }
public void dispose() { public void dispose() {
for (WindowActionManager actionManager : windowToActionManagerMap.values()) { // make sure this is on the swing thread to avoid clearing stuff while the swing update
actionManager.dispose(); // manager is firing its processContextChanged() call
} Swing.runIfSwingOrRunLater(() -> {
windowToActionManagerMap.clear(); updateManager.dispose();
for (WindowActionManager actionManager : windowToActionManagerMap.values()) {
actionManager.dispose();
}
windowToActionManagerMap.clear();
});
} }
@Override @Override
@ -113,13 +128,7 @@ public class GlobalMenuAndToolBarManager implements DockingWindowListener {
@Override @Override
public void dockingWindowFocusChanged(WindowNode windowNode) { public void dockingWindowFocusChanged(WindowNode windowNode) {
//update global menus and toolbars for this window updateManager.updateLater();
ComponentPlaceholder lastFocused = windowNode.getLastFocusedProviderInWindow();
WindowActionManager windowActionManager = windowToActionManagerMap.get(windowNode);
if (windowActionManager == null) {
return;
}
windowActionManager.contextChanged(lastFocused);
} }
private void removeWindowActionManager(WindowNode windowNode) { private void removeWindowActionManager(WindowNode windowNode) {
@ -135,8 +144,7 @@ public class GlobalMenuAndToolBarManager implements DockingWindowListener {
new WindowActionManager(windowNode, menuHandler, windowManager, menuGroupMap); new WindowActionManager(windowNode, menuHandler, windowManager, menuGroupMap);
windowToActionManagerMap.put(windowNode, newActionManager); windowToActionManagerMap.put(windowNode, newActionManager);
newActionManager.setActions(actionsForWindow); newActionManager.setActions(actionsForWindow);
ComponentPlaceholder lastFocused = windowNode.getLastFocusedProviderInWindow(); updateManager.updateLater();
newActionManager.contextChanged(lastFocused);
} }
private List<DockingActionIf> getActionsForWindow(WindowNode windowNode) { private List<DockingActionIf> getActionsForWindow(WindowNode windowNode) {
@ -152,34 +160,80 @@ public class GlobalMenuAndToolBarManager implements DockingWindowListener {
return actionsForWindow; return actionsForWindow;
} }
public void contextChangedAll() { public void contextChanged() {
Swing.runIfSwingOrRunLater(this::updateAllWindowActions); // schedule an update for all the global actions
updateManager.updateLater();
} }
private void updateAllWindowActions() { private void updateActions() {
//
// The focused window's actions must be notified after all other windows in order to
// prevent incorrect context updates. We will first update all non-focused windows,
// then the focused window and then finally tell the Docking Window Manager to update.
//
WindowNode focusedWindowNode = getFocusedWindowNode();
Set<DockingActionIf> focusedWindowActions = getWindowActions(focusedWindowNode);
ActionContext globalContext = windowManager.getDefaultToolContext();
for (WindowNode windowNode : windowToActionManagerMap.keySet()) { for (WindowNode windowNode : windowToActionManagerMap.keySet()) {
ComponentPlaceholder lastFocused = windowNode.getLastFocusedProviderInWindow(); if (windowNode == focusedWindowNode) {
windowToActionManagerMap.get(windowNode).contextChanged(lastFocused); continue; // the focused window will be called after this loop later
}
WindowActionManager actionManager = windowToActionManagerMap.get(windowNode);
ActionContext localContext = getContext(windowNode);
actionManager.contextChanged(globalContext, localContext, focusedWindowActions);
}
// now update the focused window's actions
WindowActionManager actionManager = windowToActionManagerMap.get(focusedWindowNode);
ActionContext focusedContext = getContext(focusedWindowNode);
if (actionManager != null) {
actionManager.contextChanged(globalContext, focusedContext, Collections.emptySet());
}
// update the docking window manager ; no focused context when no window is focused
if (focusedContext != null) {
windowManager.doContextChanged(focusedContext);
} }
} }
public void contextChanged(ComponentPlaceholder placeHolder) { private ActionContext getContext(WindowNode windowNode) {
if (placeHolder == null) { if (windowNode == null) {
return; return null;
} }
WindowNode topLevelNode = placeHolder.getTopLevelNode(); ActionContext context = null;
if (topLevelNode == null) { ComponentPlaceholder placeholder = windowNode.getLastFocusedProviderInWindow();
return; if (placeholder != null) {
ComponentProvider provider = placeholder.getProvider();
if (provider != null) {
context = provider.getActionContext(null);
}
} }
if (context == null) {
if (topLevelNode.getLastFocusedProviderInWindow() != placeHolder) { context = new ActionContext();
return; // actions in this window are not currently responding to this provider
}
WindowActionManager windowActionManager = windowToActionManagerMap.get(topLevelNode);
if (windowActionManager != null) {
windowActionManager.contextChanged(placeHolder);
} }
return context;
} }
private Set<DockingActionIf> getWindowActions(WindowNode windowNode) {
if (windowNode != null) {
WindowActionManager windowActionManager = windowToActionManagerMap.get(windowNode);
if (windowActionManager != null) {
return windowActionManager.getOriginalActions();
}
}
return Collections.emptySet();
}
private WindowNode getFocusedWindowNode() {
ComponentPlaceholder focusedComponent = windowManager.getFocusedComponent();
if (focusedComponent == null) {
return null;
}
return focusedComponent.getWindowNode();
}
} }

View file

@ -21,7 +21,6 @@ import javax.swing.JMenuBar;
import docking.action.DockingActionIf; import docking.action.DockingActionIf;
import docking.menu.*; import docking.menu.*;
import ghidra.util.task.SwingUpdateManager;
public class WindowActionManager { public class WindowActionManager {
private Map<DockingActionIf, DockingActionProxy> actionToProxyMap; private Map<DockingActionIf, DockingActionProxy> actionToProxyMap;
@ -29,25 +28,18 @@ public class WindowActionManager {
private ToolBarManager toolBarMgr; private ToolBarManager toolBarMgr;
private final WindowNode node; private final WindowNode node;
private final DockingWindowManager winMgr;
private boolean disposed; private boolean disposed;
private ComponentPlaceholder placeHolderForScheduledActionUpdate; WindowActionManager(WindowNode node, MenuHandler menuBarHandler,
private Runnable updateActionsRunnable = () -> processContextChanged();
private SwingUpdateManager updateManager =
new SwingUpdateManager(500, 500, "Context Update Manager", updateActionsRunnable);
public WindowActionManager(WindowNode node, MenuHandler menuBarHandler,
DockingWindowManager winMgr, MenuGroupMap menuGroupMap) { DockingWindowManager winMgr, MenuGroupMap menuGroupMap) {
this.node = node; this.node = node;
this.winMgr = winMgr;
actionToProxyMap = new HashMap<>(); actionToProxyMap = new HashMap<>();
menuBarMgr = new MenuBarManager(menuBarHandler, menuGroupMap); menuBarMgr = new MenuBarManager(menuBarHandler, menuGroupMap);
toolBarMgr = new ToolBarManager(winMgr); toolBarMgr = new ToolBarManager(winMgr);
} }
public void setActions(List<DockingActionIf> actionList) { void setActions(List<DockingActionIf> actionList) {
menuBarMgr.clearActions(); menuBarMgr.clearActions();
toolBarMgr.clearActions(); toolBarMgr.clearActions();
actionToProxyMap.clear(); actionToProxyMap.clear();
@ -56,7 +48,7 @@ public class WindowActionManager {
} }
} }
public void addAction(DockingActionIf action) { void addAction(DockingActionIf action) {
if (action.getMenuBarData() != null || action.getToolBarData() != null) { if (action.getMenuBarData() != null || action.getToolBarData() != null) {
DockingActionProxy proxyAction = new DockingActionProxy(action); DockingActionProxy proxyAction = new DockingActionProxy(action);
actionToProxyMap.put(action, proxyAction); actionToProxyMap.put(action, proxyAction);
@ -65,7 +57,7 @@ public class WindowActionManager {
} }
} }
public void removeAction(DockingActionIf action) { void removeAction(DockingActionIf action) {
DockingActionProxy proxyAction = actionToProxyMap.remove(action); DockingActionProxy proxyAction = actionToProxyMap.remove(action);
if (proxyAction != null) { if (proxyAction != null) {
menuBarMgr.removeAction(proxyAction); menuBarMgr.removeAction(proxyAction);
@ -73,11 +65,11 @@ public class WindowActionManager {
} }
} }
public DockingActionIf getToolbarAction(String actionName) { DockingActionIf getToolbarAction(String actionName) {
return toolBarMgr.getAction(actionName); return toolBarMgr.getAction(actionName);
} }
public void update() { void update() {
JMenuBar menuBar = menuBarMgr.getMenuBar(); JMenuBar menuBar = menuBarMgr.getMenuBar();
if (menuBar.getMenuCount() > 0) { if (menuBar.getMenuCount() > 0) {
node.setMenuBar(menuBar); node.setMenuBar(menuBar);
@ -87,9 +79,8 @@ public class WindowActionManager {
node.validate(); node.validate();
} }
public synchronized void dispose() { void dispose() {
disposed = true; disposed = true;
updateManager.dispose();
node.setToolBar(null); node.setToolBar(null);
node.setMenuBar(null); node.setMenuBar(null);
actionToProxyMap.clear(); actionToProxyMap.clear();
@ -97,48 +88,32 @@ public class WindowActionManager {
toolBarMgr.dispose(); toolBarMgr.dispose();
} }
synchronized void contextChanged(ComponentPlaceholder placeHolder) { void contextChanged(ActionContext globalContext, ActionContext localContext,
Set<DockingActionIf> excluded) {
if (!node.isVisible()) { if (!node.isVisible() || disposed) {
return; return;
} }
placeHolderForScheduledActionUpdate = placeHolder; // Update actions - make a copy so that we don't get concurrent modification
// exceptions during reentrant operations
// Typically, when we get one contextChanged, we get a flurry of contextChanged calls. List<DockingActionIf> list = new ArrayList<>(actionToProxyMap.keySet());
// In order to make the action updating be as responsive as possible and still be complete,
// we have chosen a policy that will reduce a flurry of contextChanged call into two
// actual calls - one that occurs immediately and one when the flurry times out.
updateManager.update();
}
private synchronized void processContextChanged() {
//
// This method is called from an invokeLater(), which means that we may be
// disposed while before this Swing call executes.
//
if (disposed) {
return;
}
ActionContext localContext = getContext();
ActionContext globalContext = winMgr.getDefaultToolContext();
// Update actions - make a copy so that we don't get concurrent modification exceptions
List<DockingActionIf> list = new ArrayList<>(actionToProxyMap.values());
for (DockingActionIf action : list) { for (DockingActionIf action : list) {
if (action.isValidContext(localContext)) { if (excluded.contains(action)) {
action.setEnabled(action.isEnabledForContext(localContext)); continue;
} }
else if (isValidDefaultToolContext(action, globalContext)) {
action.setEnabled(action.isEnabledForContext(globalContext)); DockingActionIf proxyAction = actionToProxyMap.get(action);
if (proxyAction.isValidContext(localContext)) {
proxyAction.setEnabled(proxyAction.isEnabledForContext(localContext));
}
else if (isValidDefaultToolContext(proxyAction, globalContext)) {
proxyAction.setEnabled(proxyAction.isEnabledForContext(globalContext));
} }
else { else {
action.setEnabled(false); proxyAction.setEnabled(false);
} }
} }
// Notify listeners if the context provider is the focused provider
winMgr.notifyContextListeners(placeHolderForScheduledActionUpdate, localContext);
} }
private boolean isValidDefaultToolContext(DockingActionIf action, ActionContext toolContext) { private boolean isValidDefaultToolContext(DockingActionIf action, ActionContext toolContext) {
@ -146,15 +121,15 @@ public class WindowActionManager {
action.isValidContext(toolContext); action.isValidContext(toolContext);
} }
private ActionContext getContext() { /**
ComponentProvider provider = placeHolderForScheduledActionUpdate == null ? null * Returns the set of actions for this window.
: placeHolderForScheduledActionUpdate.getProvider(); *
* <p>Note this returns the the original passed-in actions and not the proxy actions that the
ActionContext context = provider == null ? null : provider.getActionContext(null); * window uses.
*
if (context == null) { * @return the set of actions for this window
context = new ActionContext(); */
} Set<DockingActionIf> getOriginalActions() {
return context; return new HashSet<>(actionToProxyMap.keySet());
} }
} }

View file

@ -55,7 +55,7 @@ public abstract class AbstractSwingUpdateManager {
protected static final long NONE = 0; protected static final long NONE = 0;
public static final int DEFAULT_MAX_DELAY = 30000; public static final int DEFAULT_MAX_DELAY = 30000;
protected static final int MIN_DELAY_FLOOR = 10; protected static final int MIN_DELAY_FLOOR = 10;
protected static final int DEFAULT_MIN_DELAY = 250; public static final int DEFAULT_MIN_DELAY = 250;
protected static final String DEFAULT_NAME = AbstractSwingUpdateManager.class.getSimpleName(); protected static final String DEFAULT_NAME = AbstractSwingUpdateManager.class.getSimpleName();
private static final WeakSet<AbstractSwingUpdateManager> instances = private static final WeakSet<AbstractSwingUpdateManager> instances =
WeakDataStructureFactory.createCopyOnReadWeakSet(); WeakDataStructureFactory.createCopyOnReadWeakSet();