This rather large patch makes menu accelerators work in our
implementation. Attached is a demo (MenuBars.java) - while focused in
the JButton pressing CTRL + {1,2,3,4,Z,X,C,V} causes different menu
items from either the top or bottom menu to be "clicked".
The patches to ActionMap and InputMap now make the following Mauve tests
pass:
gnu/testlet/javax/swing/InputMap/newMapKeysNull
gnu/testlet/javax/swing/ActionMap/newMapKeysNull
and they also fix PR 24854.
For JMenuBar, I removed the FIXME comment in addNotify and did what it
said - register the JMenuBar with the KeyboardManager. This is because
the KeyboardManager needs to have a fast way to access all the JMenuBars
in a top-level container to give them a chance to consume any key event
that happens in that container (JMenuBars are the LAST things that get a
chance at the event). Also, JMenuBar.processKeyBinding now overrides
the method in JComponent (this agrees with the API docs) to give all its
MenuElement descendants a chance to process the binding.
JMenuItem.setAccelerator had to fire a property change event that is
handled in BasicMenuItemUI so that when you change the accelerator key
for a menuitem, its key bindings get changed accordingly (the old one
gets thrown out and the new one gets mapped propertly).
In KeyboardManager I put the new functionality it for registering
JMenuBars and also avoided some NPEs with standard != null checks.
Also, processKeyStroke now gives the JMenuBars a chance to consume the
event. Note that KeyboardManager currently only has the ability to
REGISTER bindings and JMenuBars, and not UNREGISTER them. This is an
obvious flaw that I'll get to fixing at some point.
Finally, in BasicMenuItemUI I implemented a PropertyChangeListener to
change the bindings for the accelerator keystroke, and implemented the
installKeyboardActions and uninstallKeyboardActions methods. Also made
sure that installUI calls installKeyboardActions and that
installListeners and uninstallListeners now reference the new
PropertyChangeListener.
2005-11-14 Anthony Balkissoon <[EMAIL PROTECTED]>
* javax/swing/ActionMap.java:
(keys): Return null if the map is empty.
(allKeys): Likewise.
* javax/swing/InputMap.java:
(keys): Return null if the map is empty.
(allKeys): Likewise.
* javax/swing/JMenuBar:
(addNotify): Register the menu with the KeyboardManager.
(processKeyBinding): New API method.
(processKeyBindingHelper): New implementation method.
* javax/swing/JMenuItem.java:
(setAccelerator): Fire a PropertyChangeEvent after changing the
accelerator.
* javax/swing/KeyboardManager.java:
(menuBarLookup): New field, Hashtable mapping between top-level
containers and a Vector of the JMenuBars contained in them.
(getHashtableForTopLevel): Changed this public method to package
private.
(registerEntireMap): Avoid NPE by returning early if the parameter
is null or contains no mappings.
(processKeyStroke): If the mapped component doesn't consume the event,
let all JMenuBars in the top-level container have a chance at it.
(getVectorForTopLevel): New implementation method.
(registerJMenuBar): Likewise.
* javax/swing/plaf/basic/BasicMenuItemUI.java:
(propertyChangeListener): New field.
(PropertyChangeHandler): New class to handle PropertyChangeEvents on
the JMenuItem.
(ClickAction): New class to implement accelerator key handling.
(BasicMenuItemUI<init>): Instantiate the propertyChangeListener field.
(installKeyboardActions): Implemented.
(installListeners): Install the propertyChangeListener.
(installUI): Call installKeyboardAcions after installing the listeners.
(uninstallKeyboardActions): Implemented.
(uninstallListeners): Remove the propertyChangeListener.
--Tony
Index: javax/swing/ActionMap.java
===================================================================
RCS file: /cvsroot/classpath/classpath/javax/swing/ActionMap.java,v
retrieving revision 1.12
diff -u -r1.12 ActionMap.java
--- javax/swing/ActionMap.java 19 Oct 2005 15:45:03 -0000 1.12
+++ javax/swing/ActionMap.java 14 Nov 2005 20:10:43 -0000
@@ -171,7 +171,9 @@
*/
public Object[] keys()
{
- return actionMap.keySet().toArray();
+ if (size() != 0)
+ return actionMap.keySet().toArray();
+ return null;
}
/**
@@ -188,7 +190,9 @@
set.addAll(Arrays.asList(parent.allKeys()));
set.addAll(actionMap.keySet());
- return set.toArray();
+ if (set.size() != 0)
+ return set.toArray();
+ return null;
}
/**
Index: javax/swing/InputMap.java
===================================================================
RCS file: /cvsroot/classpath/classpath/javax/swing/InputMap.java,v
retrieving revision 1.12
diff -u -r1.12 InputMap.java
--- javax/swing/InputMap.java 27 Jul 2005 12:41:33 -0000 1.12
+++ javax/swing/InputMap.java 14 Nov 2005 20:10:43 -0000
@@ -171,8 +171,12 @@
*/
public KeyStroke[] keys()
{
- KeyStroke[] array = new KeyStroke[size()];
- return (KeyStroke[]) inputMap.keySet().toArray(array);
+ if (size() != 0)
+ {
+ KeyStroke[] array = new KeyStroke[size()];
+ return (KeyStroke[]) inputMap.keySet().toArray(array);
+ }
+ return null;
}
/**
@@ -189,7 +193,9 @@
set.addAll(Arrays.asList(parent.allKeys()));
set.addAll(inputMap.keySet());
- KeyStroke[] array = new KeyStroke[size()];
+ if (set.size() == 0)
+ return null;
+ KeyStroke[] array = new KeyStroke[set.size()];
return (KeyStroke[]) set.toArray(array);
}
Index: javax/swing/JMenuBar.java
===================================================================
RCS file: /cvsroot/classpath/classpath/javax/swing/JMenuBar.java,v
retrieving revision 1.18
diff -u -r1.18 JMenuBar.java
--- javax/swing/JMenuBar.java 19 Oct 2005 15:45:04 -0000 1.18
+++ javax/swing/JMenuBar.java 14 Nov 2005 20:10:43 -0000
@@ -234,8 +234,8 @@
*/
public void addNotify()
{
- // FIXME: Should register this menu bar with the keyboard manager
super.addNotify();
+ KeyboardManager.getManager().registerJMenuBar(this);
}
public AccessibleContext getAccessibleContext()
@@ -473,6 +473,63 @@
// Do nothing - needed for implementation of MenuElement interface
}
+ /**
+ * This method overrides JComponent.processKeyBinding to allow the
+ * JMenuBar to check all the child components (recursiveley) to see
+ * if they'll consume the event.
+ *
+ * @param ks the KeyStroke for the event
+ * @param e the KeyEvent for the event
+ * @param condition the focus condition for the binding
+ * @param pressed true if the key is pressed
+ */
+ protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition,
+ boolean pressed)
+ {
+ // See if the regular JComponent behavior consumes the event
+ if (super.processKeyBinding(ks, e, condition, pressed))
+ return true;
+
+ // If not, have to recursively check all the child menu elements to see
+ // if they want it
+ MenuElement[] children = getSubElements();
+ for (int i = 0; i < children.length; i++)
+ if (processKeyBindingHelper(children[i], ks, e, condition, pressed))
+ return true;
+ return false;
+ }
+
+ /**
+ * This is a helper method to recursively check the children of this
+ * JMenuBar to see if they will consume a key event via key bindings.
+ * This is used for menu accelerators.
+ * @param menuElement the menuElement to check (and check all its children)
+ * @param ks the KeyStroke for the event
+ * @param e the KeyEvent that may be consumed
+ * @param condition the focus condition for the binding
+ * @param pressed true if the key was pressed
+ * @return true <code>menuElement</code> or one of its children consume
+ * the event (processKeyBinding returns true for menuElement or one of
+ * its children).
+ */
+ static boolean processKeyBindingHelper(MenuElement menuElement, KeyStroke ks,
+ KeyEvent e, int condition,
+ boolean pressed)
+ {
+ // First check the menuElement itself, if it's a JComponent
+ if (menuElement instanceof JComponent
+ && ((JComponent) menuElement).processKeyBinding(ks, e, condition,
+ pressed))
+ return true;
+
+ // If that didn't consume it, check all the children recursively
+ MenuElement[] children = menuElement.getSubElements();
+ for (int i = 0; i < children.length; i++)
+ if (processKeyBindingHelper(children[i], ks, e, condition, pressed))
+ return true;
+ return false;
+ }
+
/**
* Process mouse events forwarded from MenuSelectionManager. This method
* doesn't do anything. It is here to conform to the MenuElement interface.
Index: javax/swing/JMenuItem.java
===================================================================
RCS file: /cvsroot/classpath/classpath/javax/swing/JMenuItem.java,v
retrieving revision 1.22
diff -u -r1.22 JMenuItem.java
--- javax/swing/JMenuItem.java 19 Oct 2005 15:45:04 -0000 1.22
+++ javax/swing/JMenuItem.java 14 Nov 2005 20:10:43 -0000
@@ -254,7 +254,9 @@
*/
public void setAccelerator(KeyStroke keystroke)
{
+ KeyStroke old = this.accelerator;
this.accelerator = keystroke;
+ firePropertyChange ("accelerator", old, keystroke);
}
/**
Index: javax/swing/KeyboardManager.java
===================================================================
RCS file: /cvsroot/classpath/classpath/javax/swing/KeyboardManager.java,v
retrieving revision 1.2
diff -u -r1.2 KeyboardManager.java
--- javax/swing/KeyboardManager.java 10 Nov 2005 20:03:56 -0000 1.2
+++ javax/swing/KeyboardManager.java 14 Nov 2005 20:10:43 -0000
@@ -45,6 +45,7 @@
import java.awt.event.KeyEvent;
import java.util.Enumeration;
import java.util.Hashtable;
+import java.util.Vector;
/**
* This class maintains a mapping from top-level containers to a
@@ -67,6 +68,12 @@
Hashtable topLevelLookup = new Hashtable();
/**
+ * A mapping between top level containers and Vectors of JMenuBars
+ * used to allow all the JMenuBars within a top level container
+ * a chance to consume key events.
+ */
+ Hashtable menuBarLookup = new Hashtable();
+ /**
* Returns the shared instance of KeyboardManager.
* @return the shared instance of KeybaordManager.
*/
@@ -101,7 +108,7 @@
* @return the Hashtable mapping KeyStrokes to Components for the
* specified top-level container
*/
- public Hashtable getHashtableForTopLevel (Container c)
+ Hashtable getHashtableForTopLevel (Container c)
{
Hashtable keyToComponent = (Hashtable)topLevelLookup.get(c);
if (keyToComponent == null)
@@ -185,9 +192,12 @@
*/
public void registerEntireMap (ComponentInputMap map)
{
+ if (map == null)
+ return;
JComponent comp = map.getComponent();
KeyStroke[] keys = map.keys();
-
+ if (keys == null)
+ return;
// Find the top-level container associated with this ComponentInputMap
Container topLevel = findTopLevel(comp);
if (topLevel == null)
@@ -201,18 +211,56 @@
public boolean processKeyStroke (Component comp, KeyStroke key, KeyEvent e)
{
+ boolean pressed = e.getID() == KeyEvent.KEY_PRESSED;
+
// Look for the top-level ancestor
Container topLevel = findTopLevel(comp);
if (topLevel == null)
- return false;
+ return false;
// Now get the Hashtable for that top-level container
Hashtable keyToComponent = getHashtableForTopLevel(topLevel);
Enumeration keys = keyToComponent.keys();
- JComponent target = (JComponent)keyToComponent.get(key);
- if (target == null)
- return false;
- return target.processKeyBinding
- (key, e, JComponent.WHEN_IN_FOCUSED_WINDOW,
- e.getID() == KeyEvent.KEY_PRESSED);
+ JComponent target = (JComponent)keyToComponent.get(key);
+ if (target != null && target.processKeyBinding
+ (key, e, JComponent.WHEN_IN_FOCUSED_WINDOW, pressed))
+ return true;
+
+ // Have to give all the JMenuBars a chance to consume the event
+ Vector menuBars = getVectorForTopLevel(topLevel);
+ for (int i = 0; i < menuBars.size(); i++)
+ if (((JMenuBar)menuBars.elementAt(i)).processKeyBinding(key, e, JComponent.WHEN_IN_FOCUSED_WINDOW, pressed))
+ return true;
+ return false;
+ }
+
+ /**
+ * Returns the Vector of JMenuBars associated with the top-level
+ * @param c the top-level container whose JMenuBar Vector we want
+ * @return the Vector of JMenuBars for this top level container
+ */
+ Vector getVectorForTopLevel(Container c)
+ {
+ Vector result = (Vector) menuBarLookup.get(c);
+ if (result == null)
+ {
+ result = new Vector();
+ menuBarLookup.put (c, result);
+ }
+ return result;
+ }
+
+ /**
+ * In processKeyStroke, KeyManager must give all JMenuBars in the
+ * focused top-level container a chance to process the event. So,
+ * JMenuBars must be registered in KeyManager and associated with a
+ * top-level container. That's what this method is for.
+ * @param menuBar the JMenuBar to register
+ */
+ public void registerJMenuBar (JMenuBar menuBar)
+ {
+ Container topLevel = findTopLevel(menuBar);
+ Vector menuBars = getVectorForTopLevel(topLevel);
+ if (!menuBars.contains(menuBar))
+ menuBars.add(menuBar);
}
}
Index: javax/swing/plaf/basic/BasicMenuItemUI.java
===================================================================
RCS file: /cvsroot/classpath/classpath/javax/swing/plaf/basic/BasicMenuItemUI.java,v
retrieving revision 1.37
diff -u -r1.37 BasicMenuItemUI.java
--- javax/swing/plaf/basic/BasicMenuItemUI.java 7 Nov 2005 18:26:50 -0000 1.37
+++ javax/swing/plaf/basic/BasicMenuItemUI.java 14 Nov 2005 20:10:44 -0000
@@ -46,14 +46,20 @@
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
-import java.awt.event.KeyEvent;
-import java.awt.event.MouseEvent;
+import java.awt.event.ActionEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
import java.util.ArrayList;
+import javax.swing.AbstractAction;
+import javax.swing.ActionMap;
import javax.swing.ButtonModel;
import javax.swing.Icon;
+import javax.swing.InputMap;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JMenu;
@@ -72,6 +78,8 @@
import javax.swing.event.MenuKeyEvent;
import javax.swing.event.MenuKeyListener;
import javax.swing.event.MouseInputListener;
+import javax.swing.plaf.ActionMapUIResource;
+import javax.swing.plaf.ComponentInputMapUIResource;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.MenuItemUI;
@@ -173,6 +181,54 @@
*/
private int MenuGap = 10;
+ /** A PropertyChangeListener to make UI updates after property changes **/
+ PropertyChangeHandler propertyChangeListener;
+
+ /**
+ * A class to handle PropertChangeEvents for the JMenuItem
+ * @author Anthony Balkissoon abalkiss at redhat dot com.
+ */
+ class PropertyChangeHandler implements PropertyChangeListener
+ {
+ /**
+ * This method is called when a property of the menuItem is changed.
+ * Currently it is only used to update the accelerator key bindings.
+ *
+ * @param e
+ * the PropertyChangeEvent
+ */
+ public void propertyChange(PropertyChangeEvent e)
+ {
+ if (e.getPropertyName() == "accelerator")
+ {
+ InputMap map = SwingUtilities.getUIInputMap(menuItem, JComponent.WHEN_IN_FOCUSED_WINDOW);
+ if (map != null)
+ map.remove((KeyStroke)e.getOldValue());
+ else
+ map = new ComponentInputMapUIResource(menuItem);
+ map.put((KeyStroke)e.getNewValue(), "doClick");
+ }
+ }
+ }
+
+ /**
+ * A class to handle accelerator keys. This is the Action we will
+ * perform when the accelerator key for this JMenuItem is pressed.
+ * @author Anthony Balkissoon abalkiss at redhat dot com
+ *
+ */
+ class ClickAction extends AbstractAction
+ {
+ /**
+ * This is what is done when the accelerator key for the JMenuItem is
+ * pressed.
+ */
+ public void actionPerformed(ActionEvent event)
+ {
+ doClick(MenuSelectionManager.defaultManager());
+ }
+ }
+
/**
* Creates a new BasicMenuItemUI object.
*/
@@ -182,6 +238,7 @@
menuDragMouseListener = createMenuDragMouseListener(menuItem);
menuKeyListener = createMenuKeyListener(menuItem);
itemListener = new ItemHandler();
+ propertyChangeListener = new PropertyChangeHandler();
}
/**
@@ -426,7 +483,17 @@
*/
protected void installKeyboardActions()
{
- // FIXME: Need to implement
+ InputMap focusedWindowMap = SwingUtilities.getUIInputMap(menuItem, JComponent.WHEN_IN_FOCUSED_WINDOW);
+ if (focusedWindowMap == null)
+ focusedWindowMap = new ComponentInputMapUIResource(menuItem);
+ focusedWindowMap.put(menuItem.getAccelerator(), "doClick");
+ SwingUtilities.replaceUIInputMap(menuItem, JComponent.WHEN_IN_FOCUSED_WINDOW, focusedWindowMap);
+
+ ActionMap UIActionMap = SwingUtilities.getUIActionMap(menuItem);
+ if (UIActionMap == null)
+ UIActionMap = new ActionMapUIResource();
+ UIActionMap.put("doClick", new ClickAction());
+ SwingUtilities.replaceUIActionMap(menuItem, UIActionMap);
}
/**
@@ -439,6 +506,7 @@
menuItem.addMenuDragMouseListener(menuDragMouseListener);
menuItem.addMenuKeyListener(menuKeyListener);
menuItem.addItemListener(itemListener);
+ menuItem.addPropertyChangeListener(propertyChangeListener);
}
/**
@@ -456,6 +524,7 @@
installDefaults();
installComponents(menuItem);
installListeners();
+ installKeyboardActions();
}
/**
@@ -714,8 +783,9 @@
* Uninstalls any keyboard actions.
*/
protected void uninstallKeyboardActions()
- {
- // FIXME: need to implement
+ {
+ SwingUtilities.replaceUIInputMap(menuItem,
+ JComponent.WHEN_IN_FOCUSED_WINDOW, null);
}
/**
@@ -727,6 +797,7 @@
menuItem.removeMenuDragMouseListener(menuDragMouseListener);
menuItem.removeMenuKeyListener(menuKeyListener);
menuItem.removeItemListener(itemListener);
+ menuItem.removePropertyChangeListener(propertyChangeListener);
}
/**
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class MenuBars extends JMenuBar implements ActionListener
{
String[] editItems = new String[] { "Undo", "Cut", "Copy", "Paste" };
char[] editShortcuts;
public MenuBars (String name, char[] editShortcuts)
{
this.editShortcuts = editShortcuts;
JMenu editMenu = new JMenu(name);
for (int i=0; i < editItems.length; i++)
{
JMenuItem item = new JMenuItem(editItems[i]);
item.setAccelerator(KeyStroke.getKeyStroke(editShortcuts[i],
java.awt.Event.CTRL_MASK, false));
item.addActionListener(this);
editMenu.add(item);
}
add(editMenu);
}
public void actionPerformed(ActionEvent event)
{
System.out.println("Menu item [" + event.getActionCommand() +
"] was pressed.");
}
public static void main(String s[])
{
char[] editShortcuts = { 'Z','X','C','V' };
JFrame frame = new JFrame("Simple Menu Example");
frame.setSize(400,400);
frame.setJMenuBar(new MenuBars("Edit",editShortcuts));
char[] editShortcuts2 = {'1','2','3','4'};
frame.add(new MenuBars("Bottom", editShortcuts2), BorderLayout.SOUTH);
frame.add(new JButton("keyboard accelerators!"), BorderLayout.CENTER);
frame.setVisible(true);
System.out.println (frame.getLayout().getClass().getName());
}
}
_______________________________________________
Classpath-patches mailing list
[email protected]
http://lists.gnu.org/mailman/listinfo/classpath-patches