diff options
Diffstat (limited to 'libjava/classpath/javax/swing/plaf/basic/BasicPopupMenuUI.java')
-rw-r--r-- | libjava/classpath/javax/swing/plaf/basic/BasicPopupMenuUI.java | 675 |
1 files changed, 666 insertions, 9 deletions
diff --git a/libjava/classpath/javax/swing/plaf/basic/BasicPopupMenuUI.java b/libjava/classpath/javax/swing/plaf/basic/BasicPopupMenuUI.java index a26a5c7c46b..8c0fe6757e3 100644 --- a/libjava/classpath/javax/swing/plaf/basic/BasicPopupMenuUI.java +++ b/libjava/classpath/javax/swing/plaf/basic/BasicPopupMenuUI.java @@ -37,33 +37,574 @@ exception statement from your version. */ package javax.swing.plaf.basic; -import gnu.classpath.NotImplementedException; - import java.awt.Component; import java.awt.Dimension; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; +import java.util.EventListener; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ActionMap; import javax.swing.BoxLayout; +import javax.swing.InputMap; +import javax.swing.JApplet; import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; +import javax.swing.JRootPane; import javax.swing.LookAndFeel; import javax.swing.MenuElement; import javax.swing.MenuSelectionManager; import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; +import javax.swing.plaf.ActionMapUIResource; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.PopupMenuUI; - /** * UI Delegate for JPopupMenu */ public class BasicPopupMenuUI extends PopupMenuUI { + /** + * Handles keyboard navigation through menus. + */ + private static class NavigateAction + extends AbstractAction + { + + /** + * Creates a new NavigateAction instance. + * + * @param name the name of the action + */ + NavigateAction(String name) + { + super(name); + } + + /** + * Actually performs the action. + */ + public void actionPerformed(ActionEvent event) + { + String name = (String) getValue(Action.NAME); + if (name.equals("selectNext")) + navigateNextPrevious(true); + else if (name.equals("selectPrevious")) + navigateNextPrevious(false); + else if (name.equals("selectChild")) + navigateParentChild(true); + else if (name.equals("selectParent")) + navigateParentChild(false); + else if (name.equals("cancel")) + cancel(); + else if (name.equals("return")) + doReturn(); + else + assert false : "Must not reach here"; + } + + /** + * Navigates to the next or previous menu item. + * + * @param dir <code>true</code>: navigate to next, <code>false</code>: + * navigate to previous + */ + private void navigateNextPrevious(boolean dir) + { + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + MenuElement path[] = msm.getSelectedPath(); + int len = path.length; + if (len >= 2) + { + + if (path[0] instanceof JMenuBar && + path[1] instanceof JMenu && len == 2) + { + + // A toplevel menu is selected, but its popup not yet shown. + // Show the popup and select the first item + JPopupMenu popup = ((JMenu)path[1]).getPopupMenu(); + MenuElement next = + findEnabledChild(popup.getSubElements(), -1, true); + MenuElement[] newPath; + + if (next != null) + { + newPath = new MenuElement[4]; + newPath[3] = next; + } + else + { + // Menu has no enabled items, show the popup anyway. + newPath = new MenuElement[3]; + } + System.arraycopy(path, 0, newPath, 0, 2); + newPath[2] = popup; + msm.setSelectedPath(newPath); + } + else if (path[len - 1] instanceof JPopupMenu && + path[len - 2] instanceof JMenu) + { + // Select next item in already shown popup menu. + JMenu menu = (JMenu) path[len - 2]; + JPopupMenu popup = menu.getPopupMenu(); + MenuElement next = + findEnabledChild(popup.getSubElements(), -1, dir); + + if (next != null) + { + MenuElement[] newPath = new MenuElement[len + 1]; + System.arraycopy(path, 0, newPath, 0, len); + newPath[len] = next; + msm.setSelectedPath(newPath); + } + else + { + // All items in the popup are disabled. + // Find the parent popup menu and select + // its next item. If there's no parent popup menu , do nothing. + if (len > 2 && path[len - 3] instanceof JPopupMenu) + { + popup = ((JPopupMenu) path[len - 3]); + next = findEnabledChild(popup.getSubElements(), + menu, dir); + if (next != null && next != menu) + { + MenuElement[] newPath = new MenuElement[len - 1]; + System.arraycopy(path, 0, newPath, 0, len - 2); + newPath[len - 2] = next; + msm.setSelectedPath(newPath); + } + } + } + } + else + { + // Only select the next item. + MenuElement subs[] = path[len - 2].getSubElements(); + MenuElement nextChild = + findEnabledChild(subs, path[len - 1], dir); + if (nextChild == null) + { + nextChild = findEnabledChild(subs, -1, dir); + } + if (nextChild != null) + { + path[len-1] = nextChild; + msm.setSelectedPath(path); + } + } + } + } + + private MenuElement findEnabledChild(MenuElement[] children, + MenuElement start, boolean dir) + { + MenuElement found = null; + for (int i = 0; i < children.length && found == null; i++) + { + if (children[i] == start) + { + found = findEnabledChild(children, i, dir); + } + } + return found; + } + + /** + * Searches the next or previous enabled child menu element. + * + * @param children the children to search through + * @param start the index at which to start + * @param dir the direction (true == forward, false == backward) + * + * @return the found element or null + */ + private MenuElement findEnabledChild(MenuElement[] children, + int start, boolean dir) + { + MenuElement result = null; + if (dir) + { + result = findNextEnabledChild(children, start + 1, children.length-1); + if (result == null) + result = findNextEnabledChild(children, 0, start - 1); + } + else + { + result = findPreviousEnabledChild(children, start - 1, 0); + if (result == null) + result = findPreviousEnabledChild(children, children.length-1, + start + 1); + } + return result; + } + + /** + * Finds the next child element that is enabled and visible. + * + * @param children the children to search through + * @param start the start index + * @param end the end index + * + * @return the found child, or null + */ + private MenuElement findNextEnabledChild(MenuElement[] children, int start, + int end) + { + MenuElement found = null; + for (int i = start; i <= end && found == null; i++) + { + if (children[i] != null) + { + Component comp = children[i].getComponent(); + if (comp != null && comp.isEnabled() && comp.isVisible()) + { + found = children[i]; + } + } + } + return found; + } + + /** + * Finds the previous child element that is enabled and visible. + * + * @param children the children to search through + * @param start the start index + * @param end the end index + * + * @return the found child, or null + */ + private MenuElement findPreviousEnabledChild(MenuElement[] children, + int start, int end) + { + MenuElement found = null; + for (int i = start; i >= end && found == null; i--) + { + if (children[i] != null) + { + Component comp = children[i].getComponent(); + if (comp != null && comp.isEnabled() && comp.isVisible()) + { + found = children[i]; + } + } + } + return found; + } + + /** + * Navigates to the parent or child menu item. + * + * @param selectChild <code>true</code>: navigate to child, + * <code>false</code>: navigate to parent + */ + private void navigateParentChild(boolean selectChild) + { + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + MenuElement path[] = msm.getSelectedPath(); + int len = path.length; + + if (selectChild) + { + if (len > 0 && path[len - 1] instanceof JMenu + && ! ((JMenu) path[len-1]).isTopLevelMenu()) + { + // We have a submenu, open it. + JMenu menu = (JMenu) path[len - 1]; + JPopupMenu popup = menu.getPopupMenu(); + MenuElement[] subs = popup.getSubElements(); + MenuElement item = findEnabledChild(subs, -1, true); + MenuElement[] newPath; + + if (item == null) + { + newPath = new MenuElement[len + 1]; + } + else + { + newPath = new MenuElement[len + 2]; + newPath[len + 1] = item; + } + System.arraycopy(path, 0, newPath, 0, len); + newPath[len] = popup; + msm.setSelectedPath(newPath); + return; + } + } + else + { + int popupIndex = len-1; + if (len > 2 + && (path[popupIndex] instanceof JPopupMenu + || path[--popupIndex] instanceof JPopupMenu) + && ! ((JMenu) path[popupIndex - 1]).isTopLevelMenu()) + { + // We have a submenu, close it. + MenuElement newPath[] = new MenuElement[popupIndex]; + System.arraycopy(path, 0, newPath, 0, popupIndex); + msm.setSelectedPath(newPath); + return; + } + } + + // If we got here, we have not selected a child or parent. + // Check if we have a toplevel menu selected. If so, then select + // another one. + if (len > 1 && path[0] instanceof JMenuBar) + { + MenuElement currentMenu = path[1]; + MenuElement nextMenu = findEnabledChild(path[0].getSubElements(), + currentMenu, selectChild); + + if (nextMenu != null && nextMenu != currentMenu) + { + MenuElement newSelection[]; + if (len == 2) + { + // Menu is selected but its popup not shown. + newSelection = new MenuElement[2]; + newSelection[0] = path[0]; + newSelection[1] = nextMenu; + } + else + { + // Menu is selected and its popup is shown. + newSelection = new MenuElement[3]; + newSelection[0] = path[0]; + newSelection[1] = nextMenu; + newSelection[2] = ((JMenu) nextMenu).getPopupMenu(); + } + msm.setSelectedPath(newSelection); + } + } + } + + /** + * Handles cancel requests (ESC key). + */ + private void cancel() + { + // Fire popup menu cancelled event. Unfortunately the + // firePopupMenuCancelled() is protected in JPopupMenu so we work + // around this limitation by fetching the listeners and notifying them + // directly. + JPopupMenu lastPopup = (JPopupMenu) getLastPopup(); + EventListener[] ll = lastPopup.getListeners(PopupMenuListener.class); + for (int i = 0; i < ll.length; i++) + { + PopupMenuEvent ev = new PopupMenuEvent(lastPopup); + ((PopupMenuListener) ll[i]).popupMenuCanceled(ev); + } + + // Close the last popup or the whole selection if there's only one + // popup left. + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + MenuElement path[] = msm.getSelectedPath(); + if(path.length > 4) + { + MenuElement newPath[] = new MenuElement[path.length - 2]; + System.arraycopy(path,0,newPath,0,path.length-2); + MenuSelectionManager.defaultManager().setSelectedPath(newPath); + } + else + msm.clearSelectedPath(); + } + + /** + * Returns the last popup menu in the current selection or null. + * + * @return the last popup menu in the current selection or null + */ + private JPopupMenu getLastPopup() + { + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + MenuElement[] p = msm.getSelectedPath(); + JPopupMenu popup = null; + for(int i = p.length - 1; popup == null && i >= 0; i--) + { + if (p[i] instanceof JPopupMenu) + popup = (JPopupMenu) p[i]; + } + return popup; + } + + /** + * Handles ENTER key requests. This normally opens submenus on JMenu + * items, or activates the menu item as if it's been clicked on it. + */ + private void doReturn() + { + KeyboardFocusManager fmgr = + KeyboardFocusManager.getCurrentKeyboardFocusManager(); + Component focusOwner = fmgr.getFocusOwner(); + if((focusOwner == null || (focusOwner instanceof JRootPane))) + { + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + MenuElement path[] = msm.getSelectedPath(); + MenuElement lastElement; + if(path.length > 0) + { + lastElement = path[path.length - 1]; + if(lastElement instanceof JMenu) + { + MenuElement newPath[] = new MenuElement[path.length + 1]; + System.arraycopy(path,0,newPath,0,path.length); + newPath[path.length] = ((JMenu) lastElement).getPopupMenu(); + msm.setSelectedPath(newPath); + } + else if(lastElement instanceof JMenuItem) + { + JMenuItem mi = (JMenuItem)lastElement; + if (mi.getUI() instanceof BasicMenuItemUI) + { + ((BasicMenuItemUI)mi.getUI()).doClick(msm); + } + else + { + msm.clearSelectedPath(); + mi.doClick(0); + } + } + } + } + } + } + + /** + * Installs keyboard actions when a popup is opened, and uninstalls the + * keyboard actions when closed. This listens on the default + * MenuSelectionManager. + */ + private class KeyboardHelper + implements ChangeListener + { + private MenuElement[] lastSelectedPath = new MenuElement[0]; + private Component lastFocused; + private JRootPane invokerRootPane; + + public void stateChanged(ChangeEvent event) + { + MenuSelectionManager msm = (MenuSelectionManager) event.getSource(); + MenuElement[] p = msm.getSelectedPath(); + JPopupMenu popup = getActivePopup(p); + if (popup == null || popup.isFocusable()) + { + if (lastSelectedPath.length != 0 && p.length != 0 ) + { + if (! invokerEquals(p[0], lastSelectedPath[0])) + { + uninstallKeyboardActionsImpl(); + lastSelectedPath = new MenuElement[0]; + } + } + + if (lastSelectedPath.length == 0 && p.length > 0) + { + JComponent invoker; + if (popup == null) + { + if (p.length == 2 && p[0] instanceof JMenuBar + && p[1] instanceof JMenu) + { + // A menu has been selected but not opened. + invoker = (JComponent)p[1]; + popup = ((JMenu)invoker).getPopupMenu(); + } + else + { + return; + } + } + else + { + Component c = popup.getInvoker(); + if(c instanceof JFrame) + { + invoker = ((JFrame) c).getRootPane(); + } + else if(c instanceof JApplet) + { + invoker = ((JApplet) c).getRootPane(); + } + else + { + while (!(c instanceof JComponent)) + { + if (c == null) + { + return; + } + c = c.getParent(); + } + invoker = (JComponent)c; + } + } + + // Remember current focus owner. + lastFocused = KeyboardFocusManager. + getCurrentKeyboardFocusManager().getFocusOwner(); + + // Install keybindings used for menu navigation. + invokerRootPane = SwingUtilities.getRootPane(invoker); + if (invokerRootPane != null) + { + invokerRootPane.requestFocus(true); + installKeyboardActionsImpl(); + } + } + else if (lastSelectedPath.length != 0 && p.length == 0) + { + // menu hidden -- return focus to where it had been before + // and uninstall menu keybindings + uninstallKeyboardActionsImpl(); + } + } + + // Remember the last path selected + lastSelectedPath = p; + } + + private JPopupMenu getActivePopup(MenuElement[] path) + { + JPopupMenu active = null; + for (int i = path.length - 1; i >= 0 && active == null; i--) + { + MenuElement elem = path[i]; + if (elem instanceof JPopupMenu) + { + active = (JPopupMenu) elem; + } + } + return active; + } + + private boolean invokerEquals(MenuElement el1, MenuElement el2) + { + Component invoker1 = el1.getComponent(); + Component invoker2 = el2.getComponent(); + if (invoker1 instanceof JPopupMenu) + invoker1 = ((JPopupMenu) invoker1).getInvoker(); + if (invoker2 instanceof JPopupMenu) + invoker2 = ((JPopupMenu) invoker2).getInvoker(); + return invoker1 == invoker2; + } + } + /* popupMenu for which this UI delegate is for*/ protected JPopupMenu popupMenu; @@ -75,6 +616,19 @@ public class BasicPopupMenuUI extends PopupMenuUI TopWindowListener topWindowListener; /** + * Counts how many popup menus are handled by this UI or a subclass. + * This is used to install a KeyboardHelper on the MenuSelectionManager + * for the first popup, and uninstall this same KeyboardHelper when the + * last popup is uninstalled. + */ + private static int numPopups; + + /** + * This is the KeyboardHelper that listens on the MenuSelectionManager. + */ + private static KeyboardHelper keyboardHelper; + + /** * Creates a new BasicPopupMenuUI object. */ public BasicPopupMenuUI() @@ -106,6 +660,16 @@ public class BasicPopupMenuUI extends PopupMenuUI public void installUI(JComponent c) { super.installUI(c); + + // Install KeyboardHelper when the first popup is initialized. + if (numPopups == 0) + { + keyboardHelper = new KeyboardHelper(); + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + msm.addChangeListener(keyboardHelper); + } + numPopups++; + popupMenu = (JPopupMenu) c; popupMenu.setLayout(new DefaultMenuLayout(popupMenu, BoxLayout.Y_AXIS)); popupMenu.setBorderPainted(true); @@ -113,6 +677,7 @@ public class BasicPopupMenuUI extends PopupMenuUI installDefaults(); installListeners(); + installKeyboardActions(); } /** @@ -139,9 +704,77 @@ public class BasicPopupMenuUI extends PopupMenuUI * This method installs the keyboard actions for this {@link JPopupMenu}. */ protected void installKeyboardActions() - throws NotImplementedException { - // FIXME: Need to implement + // We can't install the keyboard actions here, because then all + // popup menus would have their actions registered in the KeyboardManager. + // So we install it when the popup menu is opened, and uninstall it + // when it's closed. This is done in the KeyboardHelper class. + // Install InputMap. + } + + /** + * Called by the KeyboardHandler when a popup is made visible. + */ + void installKeyboardActionsImpl() + { + Object[] bindings; + if (popupMenu.getComponentOrientation().isLeftToRight()) + { + bindings = (Object[]) + SharedUIDefaults.get("PopupMenu.selectedWindowInputMapBindings"); + } + else + { + bindings = (Object[]) SharedUIDefaults.get + ("PopupMenu.selectedWindowInputMapBindings.RightToLeft"); + } + InputMap inputMap = LookAndFeel.makeComponentInputMap(popupMenu, bindings); + SwingUtilities.replaceUIInputMap(popupMenu, + JComponent.WHEN_IN_FOCUSED_WINDOW, + inputMap); + + // Install ActionMap. + SwingUtilities.replaceUIActionMap(popupMenu, getActionMap()); + } + + /** + * Creates and returns the shared action map for JTrees. + * + * @return the shared action map for JTrees + */ + private ActionMap getActionMap() + { + ActionMap am = (ActionMap) UIManager.get("PopupMenu.actionMap"); + if (am == null) + { + am = createDefaultActions(); + UIManager.getLookAndFeelDefaults().put("PopupMenu.actionMap", am); + } + return am; + } + + /** + * Creates the default actions when there are none specified by the L&F. + * + * @return the default actions + */ + private ActionMap createDefaultActions() + { + ActionMapUIResource am = new ActionMapUIResource(); + Action action = new NavigateAction("selectNext"); + am.put(action.getValue(Action.NAME), action); + action = new NavigateAction("selectPrevious"); + am.put(action.getValue(Action.NAME), action); + action = new NavigateAction("selectParent"); + am.put(action.getValue(Action.NAME), action); + action = new NavigateAction("selectChild"); + am.put(action.getValue(Action.NAME), action); + action = new NavigateAction("return"); + am.put(action.getValue(Action.NAME), action); + action = new NavigateAction("cancel"); + am.put(action.getValue(Action.NAME), action); + + return am; } /** @@ -155,7 +788,17 @@ public class BasicPopupMenuUI extends PopupMenuUI { uninstallListeners(); uninstallDefaults(); + uninstallKeyboardActions(); popupMenu = null; + + // Install KeyboardHelper when the first popup is initialized. + numPopups--; + if (numPopups == 0) + { + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + msm.removeChangeListener(keyboardHelper); + } + } /** @@ -182,9 +825,22 @@ public class BasicPopupMenuUI extends PopupMenuUI * Uninstalls any keyboard actions. */ protected void uninstallKeyboardActions() - throws NotImplementedException { - // FIXME: Need to implement + // We can't install the keyboard actions here, because then all + // popup menus would have their actions registered in the KeyboardManager. + // So we install it when the popup menu is opened, and uninstall it + // when it's closed. This is done in the KeyboardHelper class. + // Install InputMap. + } + + /** + * Called by the KeyboardHandler when a popup is made invisible. + */ + void uninstallKeyboardActionsImpl() + { + SwingUtilities.replaceUIInputMap(popupMenu, + JComponent.WHEN_IN_FOCUSED_WINDOW, null); + SwingUtilities.replaceUIActionMap(popupMenu, null); } /** @@ -278,9 +934,10 @@ public class BasicPopupMenuUI extends PopupMenuUI // Adds topWindowListener to top-level window to listener to // ComponentEvents fired by it. We need to cancel this popup menu // if topWindow to which this popup belongs was resized or moved. - Component invoker = popupMenu.getInvoker(); + Component invoker = popupMenu.getInvoker(); Component rootContainer = SwingUtilities.getRoot(invoker); - rootContainer.addComponentListener(topWindowListener); + if (rootContainer != null) + rootContainer.addComponentListener(topWindowListener); // if this popup menu is a free floating popup menu, // then by default its first element should be always selected when |