Reputation: 11
I have a JDialog
(non modal) with a JFrame
owner. The problem I have is the that when the JDialog
has the focus it is preventing the ActionListener
associated with the JFrame
JMenuItem
accelerators getting called.
I have tried to implement a KeyEventDispatcher
in the JDialog
and redispatch the event to the JFrame
owner;
DefaultKeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(owner, e);
This did not work, as it did not trigger a call to the JMenuItem
accelerator ActionListener
.
Does anyone have a work around for this?
Cheers
Upvotes: 1
Views: 77
Reputation: 3291
As an alternative workaround, you can install Key Bindings in the modeless dialog, according to the accelerators set on the menu bar's items. Follows sample code for this, where comments on lines are added before the corresponding line:
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.util.Objects;
import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
public class Main {
/**
* A very simple {@code Action} invoking {@link AbstractButton#doClick() doClick} on the button
* given upon constructing an object of this class.
*/
private static class ClickButtonAction extends AbstractAction {
private final AbstractButton b;
public ClickButtonAction(final AbstractButton b) {
this.b = Objects.requireNonNull(b);
}
@Override
public void actionPerformed(final ActionEvent e) {
b.doClick();
}
@Override
@SuppressWarnings("CloneDeclaresCloneNotSupported")
public ClickButtonAction clone() {
try {
return (ClickButtonAction) super.clone();
}
catch (final CloneNotSupportedException cnsx) {
throw new InternalError(cnsx);
}
}
}
/**
* @param text
* @param keyCode Accelerator key code. If {@code null}, then accelerator is not set.
* {@linkplain ActionEvent#ALT_MASK ALT} is always assumed as the only modifier for the
* accelerator {@code KeyStroke}.
* @return
*/
private static JMenuItem createMenuItem(final String text,
final Integer keyCode) {
final JMenuItem menuItem = new JMenuItem(text);
menuItem.addActionListener(e -> System.out.println(e.getActionCommand()));
if (keyCode != null)
menuItem.setAccelerator(KeyStroke.getKeyStroke(keyCode, ActionEvent.ALT_MASK));
return menuItem;
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(() -> {
//Create 2 menus and a menu-bar:
final JMenu menu1 = new JMenu("Menu 1");
menu1.add(createMenuItem("A", KeyEvent.VK_A));
menu1.add(createMenuItem("B", null)); //Without accelerator.
menu1.add(createMenuItem("C", KeyEvent.VK_C));
final JMenu menu2 = new JMenu("Menu 2");
menu2.add(createMenuItem("D", KeyEvent.VK_D));
final JMenuBar bar = new JMenuBar();
bar.add(menu1);
bar.add(menu2);
//Create the button which pops the modeless dialog:
final JButton popNonModal = new JButton("Pop non modal");
//Install listener to the button:
popNonModal.addActionListener(e -> {
//Create a focusable component inside the dialog, for testing:
final JTextArea area = new JTextArea();
area.addFocusListener(new FocusListener() {
@Override
public void focusGained(final FocusEvent e) {
area.setText("I have the focus!");
}
@Override
public void focusLost(final FocusEvent e) {
area.setText("I don't have the focus.");
}
});
//Create dialog contents:
final JPanel diagContents = new JPanel(new BorderLayout());
diagContents.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
diagContents.add(area, BorderLayout.CENTER);
//Create the modeless dialog:
//Assumptions (for the object types) here!:
final JFrame frame = (JFrame) SwingUtilities.getAncestorOfClass(JFrame.class, (JComponent) e.getSource());
final JDialog diag = new JDialog(frame, "Test modeless focus", false);
//Transfer owning frame's menu accelerators to the dialog:
/*Will install bindings in the root pane, because this is the root component of the
dialog (and we need to be dialog-wide sensitive). AFAIK dialogs don't have key
bindings themselves (since they are not JComponents).*/
final JRootPane diagRootPane = diag.getRootPane();
//Also works with 'WHEN_ANCESTOR_OF_FOCUSED_COMPONENT', since we are in the root pane:
final InputMap diagInputMap = diagRootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
final ActionMap diagActionMap = diagRootPane.getActionMap();
transferMenuBarAccelerators(bar, diagInputMap, diagActionMap);
//Show the modeless dialog:
diag.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
diag.add(diagContents);
diag.pack();
diag.setLocationRelativeTo(frame);
diag.setVisible(true);
});
//Create frame contents:
final JPanel contents = new JPanel();
contents.add(popNonModal);
//Create and show the frame:
final JFrame frame = new JFrame("Accelerating the frame");
frame.setJMenuBar(bar);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(contents);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
/**
* Creates key bindings for each accelerator of the {@code source} {@code JMenuBar}, using the
* given {@code InputMap} and {@code ActionMap}. The process is applied for each
* {@code JMenuItem} for each {@code JMenu} of {@code source}.
* @param source The method obtains menus and items from this parameter.
* @param targetInputMap The method puts accelerators here.
* @param targetActionMap The method puts click actions here.
*/
private static void transferMenuBarAccelerators(final JMenuBar source,
final InputMap targetInputMap,
final ActionMap targetActionMap) {
/*Traversing menus and items is implemented with a backward loop, for no serious reasons,
other than only to not call 'get*Count()' methods more than once, and also not to create
additional variables (with eg 'final int count = source.getMenuCount();').*/
for (int menuIndex = source.getMenuCount() - 1; menuIndex >= 0; --menuIndex) {
final JMenu menu = source.getMenu(menuIndex);
for (int itemIndex = menu.getItemCount() - 1; itemIndex >= 0; --itemIndex) {
final JMenuItem item = menu.getItem(itemIndex);
//Consider also 'item.getAction().getValue(Action.ACCELERATOR_KEY);', but 'getAction()' may return null...
final KeyStroke accelerator = item.getAccelerator();
//We may not have an accelerator currently for the item:
if (accelerator != null) {
//Create a system-wide unique object for the key:
final Object key = new Object();
targetInputMap.put(accelerator, key);
//Consider also 'item.getAction()', but this may return null...
targetActionMap.put(key, new ClickButtonAction(item));
}
}
}
}
}
Click on the frame's button to pop the modeless dialog. A text-field in the dialog can help to transfer focus to the dialog if needed. Then you can test accelerators from the dialog (in this case Alt + A, Alt + C, Alt + D).
Nevertheless this is a mostly manual let's say process:
ActionMap
's key+Action
pair.To solve the above two problems, I tried to set the dialog's InputMap
's parent to the frame's JMenuBar
s InputMap
, but this is not feasible since it is required internally that the two involved InputMap
s refer to the same Component
(a corresponding Exception
will be thrown if you try this).
According to the documentation of JMenuItem.setAccelerator
:
It is the UI's responsibility to install the correct action.
So, to solve point (2) only, there may be a possibility to search for the key+Action
Key Binding pair that the UI installs, since you have the KeyStroke
accelerator of your menu item and an InputMap
hierarchy for the menu item to work with. But this doesn't sound good for portability between environments (since it depends on how the implementation of the UI installs the Action
, which is something we cannot control and depends on the environment).
Finally, this workaround stands only in cases where you install the accelerator on each JMenuItem
and never change it again throughout the runtime. This may be mitigated however if one properly monitors the JMenuItem
's accelerator property (with a corresponding PropertyChangeListener
). It is a little confusing though, at least for me, the fact that the JMenuItem
has an accelerator, but so does the menu item's Action
itself (via action.getValue(Action.ACCELERATOR_KEY)
). So, I don't really know if one would have to monitor the menu item's accelerator, along with the action's accelerator (and the action itself, as a property of the menu item).
Note: this source code is Java 8, but the documentation link is for 23 (where the quote still stands).
Upvotes: 0
Reputation: 11
Thank you @abra.
I was able to find a work around for this issue.
By using the KeyEvent
argument of the dispatchKeyEvent()
method from the KeyEventDispatcher
interface I was able to determine the associated KeyStroke
used to generate the KeyEvent
. This enabled me to find the JMenuItem
(if any) in the application's main JFrame
with an associated KeyStroke
accelerator. Once I have the JMenuItem
associated with the KeyStroke
, it can be programmatically activated by the JMenuItem.doClick()
method.
This effectively redispatched the KeyEvent
from the child JDialog
to the application's main JFrame
.
I suppose KeyBindings
could have solved this issue but as this is legacy code, reworking the Menu
structure of the application main JFrame
would have been a less optimal solution.
Upvotes: 0