/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Tiny Look and Feel * * * * (C) Copyright 2003 - 2007 Hans Bickel * * * * For licensing information and credits, please refer to the * * comment in file de.muntjak.tinylookandfeel.TinyLookAndFeel * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package de.muntjak.tinylookandfeel; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.KeyEventPostProcessor; import java.awt.Point; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.AbstractAction; import javax.swing.AbstractButton; import javax.swing.ActionMap; 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.KeyStroke; import javax.swing.MenuElement; import javax.swing.MenuSelectionManager; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.event.ChangeListener; import javax.swing.event.MenuDragMouseEvent; import javax.swing.event.MenuDragMouseListener; import javax.swing.event.MenuEvent; import javax.swing.event.MenuKeyEvent; import javax.swing.event.MenuKeyListener; import javax.swing.event.MenuListener; import javax.swing.event.MouseInputListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.ComboPopup; /** * A default L&F implementation of MenuUI. This implementation * is a "combined" view/controller. * * @version 1.144 04/24/02 * @author Georges Saab * @author David Karlton * @author Arnaud Weber */ public class TinyMenuUI extends TinyMenuItemUI { /* diagnostic aids -- should be false for production builds. */ private static final boolean TRACE = false; // trace creates and disposes private static final boolean VERBOSE = false; // show reuse hits/misses private static final boolean DEBUG = false; // show bad params, misc. static final AltProcessor altProcessor = new AltProcessor(); protected ChangeListener changeListener; protected PropertyChangeListener propertyChangeListener; protected MenuListener menuListener; private int lastMnemonic = 0; private static boolean crossMenuMnemonic = true; // Shared instance of the menuListener private static MenuListener sharedMenuListener; public static ComponentUI createUI(JComponent x) { return new TinyMenuUI(); } protected void installDefaults() { super.installDefaults(); ((JMenu)menuItem).setDelay(200); crossMenuMnemonic = UIManager.getBoolean("Menu.crossMenuMnemonic"); // ((JMenu)menuItem).addMenuListener(new MenuListener() { // public void menuSelected(MenuEvent e) {} // // public void menuDeselected(MenuEvent e) { // Thread.dumpStack(); // } // // public void menuCanceled(MenuEvent e) {} // }); } /** * The ActionMap for BasicMenUI can not be shared, this is subclassed * to create a new one for each invocation. */ ActionMap getActionMap() { return createActionMap(); } /** * Invoked to create the ActionMap. */ ActionMap createActionMap() { ActionMap am = super.createActionMap(); if(am != null) { am.put("selectMenu", new PostAction((JMenu) menuItem, true)); } return am; } protected String getPropertyPrefix() { return "Menu"; } protected void installListeners() { super.installListeners(); if(changeListener == null) changeListener = createChangeListener(menuItem); if(changeListener != null) menuItem.addChangeListener(changeListener); if(propertyChangeListener == null) propertyChangeListener = createPropertyChangeListener(menuItem); if(propertyChangeListener != null) menuItem.addPropertyChangeListener(propertyChangeListener); // Removed in 1.3.6 because installing additional listeners // is unnecessary and caused malfunctions with sub menus // if(menuListener == null) // menuListener = createMenuListener(menuItem); // // if(menuListener != null) // ((JMenu) menuItem).addMenuListener(menuListener); // Note: We install the MenuKeyListener only if we are // running Java 1.4, with Java 1.5 and higher, mnemonics // are handled in BasicPopupMenuUI if(TinyLookAndFeel.is1dot4()) { if((menuKeyListener = createMenuKeyListener(menuItem)) != null) { menuItem.addMenuKeyListener(menuKeyListener); } } } protected void installKeyboardActions() { super.installKeyboardActions(); // installs only ActionMap updateMnemonicBinding(); } void updateMnemonicBinding() { int mnemonic = menuItem.getModel().getMnemonic(); int[] shortcutKeys = (int[]) UIManager.get("Menu.shortcutKeys"); if(mnemonic == lastMnemonic || shortcutKeys == null) { return; } if(lastMnemonic != 0 && windowInputMap != null) { for (int i = 0; i < shortcutKeys.length; i++) { windowInputMap.remove(KeyStroke.getKeyStroke(lastMnemonic, shortcutKeys[i], false)); } } if(mnemonic != 0) { if(windowInputMap == null) { windowInputMap = createInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); SwingUtilities.replaceUIInputMap(menuItem, JComponent.WHEN_IN_FOCUSED_WINDOW, windowInputMap); } for (int i = 0; i < shortcutKeys.length; i++) { windowInputMap.put(KeyStroke.getKeyStroke(mnemonic, shortcutKeys[i], false), "selectMenu"); } } lastMnemonic = mnemonic; } protected MouseInputListener createMouseInputListener(JComponent c) { return new MouseInputHandler(); } protected ChangeListener createChangeListener(JComponent c) { return null; } protected PropertyChangeListener createPropertyChangeListener(JComponent c) { return new PropertyChangeHandler(); } protected void uninstallDefaults() { menuItem.setArmed(false); menuItem.setSelected(false); menuItem.resetKeyboardActions(); super.uninstallDefaults(); } protected void uninstallListeners() { super.uninstallListeners(); if(changeListener != null) { menuItem.removeChangeListener(changeListener); changeListener = null; } if(propertyChangeListener != null) { menuItem.removePropertyChangeListener(propertyChangeListener); propertyChangeListener = null; } if(menuKeyListener != null) { menuItem.removeMenuKeyListener(menuKeyListener); menuKeyListener = null; } } protected MenuDragMouseListener createMenuDragMouseListener(JComponent c) { return new MenuDragMouseHandler(); } protected MenuKeyListener createMenuKeyListener(JComponent c) { return new MenuKeyHandler(); } // protected MenuListener createMenuListener(JComponent c) { // if(sharedMenuListener == null) { // sharedMenuListener = new MenuHandler(); // } // return sharedMenuListener; // } public Dimension getMaximumSize(JComponent c) { if(((JMenu) menuItem).isTopLevelMenu() == true) { Dimension d = c.getPreferredSize(); return new Dimension(d.width, Short.MAX_VALUE); } return null; } protected void setupPostTimer(JMenu menu) { Timer timer = new Timer(menu.getDelay(), new PostAction(menu, false)); timer.setRepeats(false); timer.start(); } private static void appendPath(MenuElement[] path, MenuElement elem) { MenuElement newPath[] = new MenuElement[path.length + 1]; System.arraycopy(path, 0, newPath, 0, path.length); newPath[path.length] = elem; MenuSelectionManager.defaultManager().setSelectedPath(newPath); } private static class PostAction extends AbstractAction { JMenu menu; boolean force = false; PostAction(JMenu menu, boolean shouldForce) { this.menu = menu; this.force = shouldForce; } public void actionPerformed(ActionEvent e) { if(!crossMenuMnemonic) { JPopupMenu pm = getActivePopupMenu(); if(pm != null && pm != menu.getParent()) { return; } } final MenuSelectionManager defaultManager = MenuSelectionManager.defaultManager(); if(force) { Container cnt = menu.getParent(); if(cnt != null && cnt instanceof JMenuBar) { MenuElement me[]; MenuElement subElements[]; subElements = menu.getPopupMenu().getSubElements(); if(subElements.length > 0) { me = new MenuElement[4]; me[0] = (MenuElement) cnt; me[1] = (MenuElement) menu; me[2] = (MenuElement) menu.getPopupMenu(); me[3] = subElements[0]; } else { me = new MenuElement[3]; me[0] = (MenuElement) cnt; me[1] = menu; me[2] = (MenuElement) menu.getPopupMenu(); } defaultManager.setSelectedPath(me); } } else { MenuElement path[] = defaultManager.getSelectedPath(); if(path.length > 0 && path[path.length - 1] == menu) { appendPath(path, menu.getPopupMenu()); } } } public boolean isEnabled() { return menu.getModel().isEnabled(); } } private class PropertyChangeHandler implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent e) { String prop = e.getPropertyName(); if(prop.equals(AbstractButton.MNEMONIC_CHANGED_PROPERTY)) { updateMnemonicBinding(); } } } /** * Instantiated and used by a menu item to handle the current menu selection * from mouse events. A MouseInputHandler processes and forwards all mouse events * to a shared instance of the MenuSelectionManager. * <p> * This class is protected so that it can be subclassed by other look and * feels to implement their own mouse handling behavior. All overridden * methods should call the parent methods so that the menu selection * is correct. * * @see javax.swing.MenuSelectionManager * @since 1.4 */ protected class MouseInputHandler implements MouseInputListener { public void mouseClicked(MouseEvent e) { } /** * Invoked when the mouse has been clicked on the menu. This * method clears or sets the selection path of the * MenuSelectionManager. * * @param e the mouse event */ public void mousePressed(MouseEvent e) { JMenu menu = (JMenu) menuItem; if(!menu.isEnabled()) return; MenuSelectionManager manager = MenuSelectionManager.defaultManager(); if(menu.isTopLevelMenu()) { if(menu.isSelected()) { manager.clearSelectedPath(); } else { Container cnt = menu.getParent(); if(cnt != null && cnt instanceof JMenuBar) { MenuElement me[] = new MenuElement[2]; me[0] = (MenuElement) cnt; me[1] = menu; manager.setSelectedPath(me); } } } MenuElement selectedPath[] = manager.getSelectedPath(); if(selectedPath.length > 0 && selectedPath[selectedPath.length - 1] != menu.getPopupMenu()) { if(menu.isTopLevelMenu() || menu.getDelay() == 0) { appendPath(selectedPath, menu.getPopupMenu()); } else { setupPostTimer(menu); } } } /** * Invoked when the mouse has been released on the menu. Delegates the * mouse event to the MenuSelectionManager. * * @param e the mouse event */ public void mouseReleased(MouseEvent e) { JMenu menu = (JMenu) menuItem; if(!menu.isEnabled()) return; MenuSelectionManager manager = MenuSelectionManager.defaultManager(); manager.processMouseEvent(e); if(!e.isConsumed()) manager.clearSelectedPath(); } /** * Invoked when the cursor enters the menu. This method sets the selected * path for the MenuSelectionManager and handles the case * in which a menu item is used to pop up an additional menu, as in a * hierarchical menu system. * * @param e the mouse event; not used */ public void mouseEntered(MouseEvent e) { JMenu menu = (JMenu) menuItem; if(!menu.isEnabled() || menu.getClientProperty("isSystemMenu") == Boolean.TRUE) { return; } menu.putClientProperty("rollover", Boolean.TRUE); MenuSelectionManager manager = MenuSelectionManager.defaultManager(); MenuElement selectedPath[] = manager.getSelectedPath(); if(!menu.isTopLevelMenu()) { if(!(selectedPath.length > 0 && selectedPath[selectedPath.length - 1] == menu.getPopupMenu())) { if(menu.getDelay() == 0) { appendPath(getPath(), menu.getPopupMenu()); } else { manager.setSelectedPath(getPath()); setupPostTimer(menu); } } } else { if(selectedPath.length > 0 && selectedPath[0] == menu.getParent()) { MenuElement newPath[] = new MenuElement[3]; // A top level menu's parent is by definition // a JMenuBar newPath[0] = (MenuElement) menu.getParent(); newPath[1] = menu; newPath[2] = menu.getPopupMenu(); manager.setSelectedPath(newPath); } } if(menu.isTopLevelMenu()) { menu.repaint(); } } public void mouseExited(MouseEvent e) { JMenu menu = (JMenu) menuItem; if(!menu.isEnabled() || menu.getClientProperty("isSystemMenu") == Boolean.TRUE) { return; } menu.putClientProperty("rollover", Boolean.FALSE); if(menu.isTopLevelMenu()) { menu.repaint(); } } /** * Invoked when a mouse button is pressed on the menu and then dragged. * Delegates the mouse event to the MenuSelectionManager. * * @param e the mouse event * @see java.awt.event.MouseMotionListener#mouseDragged */ public void mouseDragged(MouseEvent e) { JMenu menu = (JMenu) menuItem; if(!menu.isEnabled()) return; MenuSelectionManager.defaultManager().processMouseEvent(e); } public void mouseMoved(MouseEvent e) { } } private class MenuDragMouseHandler implements MenuDragMouseListener { public void menuDragMouseEntered(MenuDragMouseEvent e) { } public void menuDragMouseDragged(MenuDragMouseEvent e) { if(menuItem.isEnabled() == false) return; MenuSelectionManager manager = e.getMenuSelectionManager(); MenuElement path[] = e.getPath(); Point p = e.getPoint(); if(p.x >= 0 && p.x < menuItem.getWidth() && p.y >= 0 && p.y < menuItem.getHeight()) { JMenu menu = (JMenu) menuItem; MenuElement selectedPath[] = manager.getSelectedPath(); if(!(selectedPath.length > 0 && selectedPath[selectedPath.length - 1] == menu.getPopupMenu())) { if(menu.isTopLevelMenu() || menu.getDelay() == 0 || e.getID() == MouseEvent.MOUSE_DRAGGED) { appendPath(path, menu.getPopupMenu()); } else { manager.setSelectedPath(path); setupPostTimer(menu); } } } else if(e.getID() == MouseEvent.MOUSE_RELEASED) { Component comp = manager.componentForPoint(e.getComponent(), e.getPoint()); if(comp == null) manager.clearSelectedPath(); } } public void menuDragMouseExited(MenuDragMouseEvent e) { } public void menuDragMouseReleased(MenuDragMouseEvent e) { } } static JPopupMenu getActivePopupMenu() { MenuElement[] path = MenuSelectionManager.defaultManager().getSelectedPath(); for (int i = path.length - 1; i >= 0; i--) { MenuElement elem = path[i]; if(elem instanceof JPopupMenu) { return (JPopupMenu) elem; } } return null; } // Currently not installed (1.3.6) private static class MenuHandler implements MenuListener { public void menuSelected(MenuEvent e) {} public void menuDeselected(MenuEvent e) {} public void menuCanceled(MenuEvent e) { JMenu m = (JMenu) e.getSource(); MenuSelectionManager manager = MenuSelectionManager.defaultManager(); if(manager.isComponentPartOfCurrentMenu(m)) MenuSelectionManager.defaultManager().clearSelectedPath(); } } /** * Handles the mnemonic handling for the JMenu and JMenuItems. */ private class MenuKeyHandler implements MenuKeyListener { // fields for handling duplicate mnemonics. private int indexes[]; private char lastMnemonic; private int lastIndex; private int matches; /** * Opens the SubMenu */ public void menuKeyTyped(MenuKeyEvent e) { if(menuItem == null) return; if(DEBUG) { System.out.println("in TinyMenuUI.menuKeyTyped for " + menuItem.getText()); } if(!crossMenuMnemonic) { JPopupMenu pm = getActivePopupMenu(); if(pm != null && pm != menuItem.getParent()) { return; } } int key = menuItem.getMnemonic(); if(key == 0) return; MenuElement path[] = e.getPath(); if(lower((char) key) == lower(e.getKeyChar())) { JPopupMenu popupMenu = ((JMenu) menuItem).getPopupMenu(); MenuElement sub[] = popupMenu.getSubElements(); if(sub.length > 0) { MenuSelectionManager manager = e.getMenuSelectionManager(); MenuElement newPath[] = new MenuElement[path.length + 2]; System.arraycopy(path, 0, newPath, 0, path.length); newPath[path.length] = popupMenu; newPath[path.length + 1] = sub[0]; manager.setSelectedPath(newPath); } e.consume(); } } /** * Handles the mnemonics for the menu items. Will also handle duplicate mnemonics. * Perhaps this should be moved into BasicPopupMenuUI. See 4670831 */ public void menuKeyPressed(MenuKeyEvent e) { if(menuItem == null) return; if(DEBUG) { System.out.println("in TinyMenuUI.menuKeyPressed for " + menuItem.getText()); } // Handle the case for Escape or Enter... char keyChar = e.getKeyChar(); if(!Character.isLetterOrDigit(keyChar)) return; MenuSelectionManager manager = e.getMenuSelectionManager(); MenuElement path[] = e.getPath(); MenuElement selectedPath[] = manager.getSelectedPath(); for (int i = selectedPath.length - 1; i >= 0; i--) { if(selectedPath[i] == menuItem) { JPopupMenu popupMenu = ((JMenu) menuItem).getPopupMenu(); MenuElement items[] = popupMenu.getSubElements(); if(indexes == null || lastMnemonic != keyChar) { matches = 0; lastIndex = 0; indexes = new int[items.length]; for (int j = 0; j < items.length; j++) { int key = ((JMenuItem) items[j]).getMnemonic(); if(lower((char) key) == lower(keyChar)) { indexes[matches++] = j; } } lastMnemonic = keyChar; } if(matches == 0) { ; // no op (consume) } else if(matches == 1) { // Invoke the menu action JMenuItem item = (JMenuItem) items[indexes[0]]; if(!(item instanceof JMenu)) { // Let Submenus be handled by menuKeyTyped manager.clearSelectedPath(); item.doClick(); } } else { // Select the menu item with the matching mnemonic. If // the same mnemonic has been invoked then select the next // menu item in the cycle. if(lastIndex == matches) { // Take care of the situation in which the // mnemonic wraps. lastIndex = 0; } MenuElement menuItem = items[indexes[lastIndex++]]; MenuElement newPath[] = new MenuElement[path.length + 2]; System.arraycopy(path, 0, newPath, 0, path.length); newPath[path.length] = popupMenu; newPath[path.length + 1] = menuItem; manager.setSelectedPath(newPath); } e.consume(); return; } } } public void menuKeyReleased(MenuKeyEvent e) {} private char lower(char keyChar) { return Character.toLowerCase(keyChar); } } static class AltProcessor implements KeyEventPostProcessor { static boolean altKeyPressed = false; static boolean menuCanceledOnPress = false; static JRootPane root = null; static Window winAncestor = null; void altPressed(KeyEvent ev) { MenuSelectionManager msm = MenuSelectionManager.defaultManager(); MenuElement[] path = msm.getSelectedPath(); if(path.length > 0 && ! (path[0] instanceof ComboPopup)) { msm.clearSelectedPath(); menuCanceledOnPress = true; ev.consume(); } else if(path.length > 0) { // We are in ComboBox menuCanceledOnPress = false; ev.consume(); } else { menuCanceledOnPress = false; JMenuBar mbar = root != null ? root.getJMenuBar() : null; if(mbar == null && winAncestor instanceof JFrame) { mbar = ((JFrame)winAncestor).getJMenuBar(); } JMenu menu = mbar != null ? mbar.getMenu(0) : null; if(menu != null) { ev.consume(); } } } void altReleased(KeyEvent ev) { if(menuCanceledOnPress) { return; } MenuSelectionManager msm = MenuSelectionManager.defaultManager(); if(msm.getSelectedPath().length == 0) { // if no menu is active, we try activating the menubar JMenuBar mbar = root != null ? root.getJMenuBar() : null; if(mbar == null && winAncestor instanceof JFrame) { mbar = ((JFrame)winAncestor).getJMenuBar(); } JMenu menu = mbar != null ? mbar.getMenu(0) : null; if(menu != null) { MenuElement[] path = new MenuElement[2]; path[0] = mbar; path[1] = menu; msm.setSelectedPath(path); } } } public boolean postProcessKeyEvent(KeyEvent ev) { if(ev.getKeyCode() == KeyEvent.VK_ALT) { root = SwingUtilities.getRootPane(ev.getComponent()); winAncestor = SwingUtilities.getWindowAncestor(root); if(ev.getID() == KeyEvent.KEY_PRESSED) { if(!altKeyPressed) { altPressed(ev); } altKeyPressed = true; return true; } else if(ev.getID() == KeyEvent.KEY_RELEASED) { if(altKeyPressed) { altReleased(ev); } altKeyPressed = false; } } else { altKeyPressed = false; } return false; } } }