/* * Copyright 2002-2006 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, * CA 95054 USA or visit www.sun.com if you need additional information or * have any questions. */ package javax.swing.plaf.synth; import javax.swing.plaf.basic.BasicHTML; import java.awt.*; import java.awt.event.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.*; import javax.swing.event.*; import javax.swing.border.*; import javax.swing.plaf.*; import javax.swing.plaf.basic.*; import javax.swing.text.View; import sun.swing.plaf.synth.*; import sun.swing.SwingUtilities2; /** * Synth's MenuItemUI. * * @author Georges Saab * @author David Karlton * @author Arnaud Weber * @author Fredrik Lagerblad */ class SynthMenuItemUI extends BasicMenuItemUI implements PropertyChangeListener, SynthUI { private SynthStyle style; private SynthStyle accStyle; private String acceleratorDelimiter; public static ComponentUI createUI(JComponent c) { return new SynthMenuItemUI(); } // // The next handful of static methods are used by both SynthMenuUI // and SynthMenuItemUI. This is necessitated by SynthMenuUI not // extending SynthMenuItemUI. // /* * All JMenuItems (and JMenus) include enough space for the insets * plus one or more elements. When we say "icon(s)" below, we mean * "check/radio indicator and/or user icon." If both are defined for * a given menu item, then in a LTR orientation the check/radio indicator * is on the left side followed by the user icon to the right; it is * just the opposite in a RTL orientation. * * Cases to consider for SynthMenuItemUI (visualized here in a * LTR orientation; the RTL case would be reversed): * text * icon(s) + text * icon(s) + text + accelerator * text + accelerator * * Cases to consider for SynthMenuUI (again visualized here in a * LTR orientation): * text + arrow * (user)icon + text + arrow * * Note that in the above scenarios, accelerator and arrow icon are * mutually exclusive. This means that if a popup menu contains a mix * of JMenus and JMenuItems, we only need to allow enough space for * max(maxAccelerator, maxArrow), and both accelerators and arrow icons * can occupy the same "column" of space in the menu. * * A quick note about how preferred sizes are calculated... Generally * speaking, SynthPopupMenuUI will run through the list of its children * (from top to bottom) and ask each for its preferred size. Each menu * item will add up the max width of each element (icons, text, * accelerator spacing, accelerator text or arrow icon) encountered thus * far, so by the time all menu items have been calculated, we will * know the maximum (preferred) menu item size for that popup menu. * Later when it comes time to paint each menu item, we can use those * same accumulated max element sizes in order to layout the item. */ static Dimension getPreferredMenuItemSize(SynthContext context, SynthContext accContext, JComponent c, Icon checkIcon, Icon arrowIcon, int defaultTextIconGap, String acceleratorDelimiter) { JMenuItem b = (JMenuItem) c; Icon icon = (Icon) b.getIcon(); String text = b.getText(); KeyStroke accelerator = b.getAccelerator(); String acceleratorText = ""; if (accelerator != null) { int modifiers = accelerator.getModifiers(); if (modifiers > 0) { acceleratorText = KeyEvent.getKeyModifiersText(modifiers); acceleratorText += acceleratorDelimiter; } int keyCode = accelerator.getKeyCode(); if (keyCode != 0) { acceleratorText += KeyEvent.getKeyText(keyCode); } else { acceleratorText += accelerator.getKeyChar(); } } Font font = context.getStyle().getFont(context); FontMetrics fm = b.getFontMetrics(font); FontMetrics fmAccel = b.getFontMetrics(accContext.getStyle(). getFont(accContext)); resetRects(); layoutMenuItem( context, fm, accContext, text, fmAccel, acceleratorText, icon, checkIcon, arrowIcon, b.getVerticalAlignment(), b.getHorizontalAlignment(), b.getVerticalTextPosition(), b.getHorizontalTextPosition(), viewRect, iconRect, textRect, acceleratorRect, checkIconRect, arrowIconRect, text == null ? 0 : defaultTextIconGap, defaultTextIconGap); r.setBounds(textRect); int totalIconWidth = 0; int maxIconHeight = 0; if (icon != null) { // Add in the user icon totalIconWidth += iconRect.width; if (textRect.width > 0) { // Allow for some room between the user icon and the text totalIconWidth += defaultTextIconGap; } maxIconHeight = Math.max(iconRect.height, maxIconHeight); } if (checkIcon != null) { // Add in the checkIcon totalIconWidth += checkIconRect.width; if (textRect.width > 0 || icon != null) { // Allow for some room between the check/radio indicator // and the text (or user icon, if both are specified) totalIconWidth += defaultTextIconGap; } maxIconHeight = Math.max(checkIconRect.height, maxIconHeight); } int arrowWidth = 0; if (arrowIcon != null) { // Add in the arrowIcon arrowWidth += defaultTextIconGap; arrowWidth += arrowIconRect.width; maxIconHeight = Math.max(arrowIconRect.height, maxIconHeight); } int accelSpacing = 0; if (acceleratorRect.width > 0) { // Allow for some room between the text and the accelerator accelSpacing += 4*defaultTextIconGap; } // Take text and all icons into account when determining height r.height = Math.max(r.height, maxIconHeight); // To make the accelerator texts appear in a column, // find the widest MenuItem text and the widest accelerator text. // Get the parent, which stores the information. Container parent = b.getParent(); if (parent instanceof JPopupMenu) { SynthPopupMenuUI popupUI = (SynthPopupMenuUI)SynthLookAndFeel. getUIOfType(((JPopupMenu)parent).getUI(), SynthPopupMenuUI.class); if (popupUI != null) { // This gives us the widest MenuItem text encountered thus // far in the parent JPopupMenu r.width = popupUI.adjustTextWidth(r.width); // Add in the widest icon (includes both user and // check/radio icons) encountered thus far r.width += popupUI.adjustIconWidth(totalIconWidth); // Add in the widest text/accelerator spacing // encountered thus far r.width += popupUI.adjustAccelSpacingWidth(accelSpacing); // Add in the widest accelerator text (or arrow) // encountered thus far (at least one of these values // will always be zero, so we combine them here to // avoid double counting) int totalAccelOrArrow = acceleratorRect.width + arrowWidth; r.width += popupUI.adjustAcceleratorWidth(totalAccelOrArrow); } } else if (parent != null && !(b instanceof JMenu && ((JMenu)b).isTopLevelMenu())) { r.width += totalIconWidth + accelSpacing + acceleratorRect.width + arrowWidth; } Insets insets = b.getInsets(); if(insets != null) { r.width += insets.left + insets.right; r.height += insets.top + insets.bottom; } // if the width is even, bump it up one. This is critical // for the focus dash line to draw properly if(r.width%2 == 0) { r.width++; } // if the height is even, bump it up one. This is critical // for the text to center properly if(r.height%2 == 0) { r.height++; } return r.getSize(); } static void paint(SynthContext context, SynthContext accContext, Graphics g, Icon checkIcon, Icon arrowIcon, String acceleratorDelimiter, int defaultTextIconGap) { JComponent c = context.getComponent(); JMenuItem b = (JMenuItem)c; ButtonModel model = b.getModel(); Insets i = b.getInsets(); resetRects(); viewRect.setBounds(0, 0, b.getWidth(), b.getHeight()); viewRect.x += i.left; viewRect.y += i.top; viewRect.width -= (i.right + viewRect.x); viewRect.height -= (i.bottom + viewRect.y); SynthStyle style = context.getStyle(); Font f = style.getFont(context); g.setFont(f); FontMetrics fm = SwingUtilities2.getFontMetrics(c, g, f); FontMetrics accFM = SwingUtilities2.getFontMetrics(c, g, accContext.getStyle(). getFont(accContext)); // get Accelerator text KeyStroke accelerator = b.getAccelerator(); String acceleratorText = ""; if (accelerator != null) { int modifiers = accelerator.getModifiers(); if (modifiers > 0) { acceleratorText = KeyEvent.getKeyModifiersText(modifiers); acceleratorText += acceleratorDelimiter; } int keyCode = accelerator.getKeyCode(); if (keyCode != 0) { acceleratorText += KeyEvent.getKeyText(keyCode); } else { acceleratorText += accelerator.getKeyChar(); } } // Layout the text and icon String text = layoutMenuItem(context, fm, accContext, b.getText(), accFM, acceleratorText, b.getIcon(), checkIcon, arrowIcon, b.getVerticalAlignment(), b.getHorizontalAlignment(), b.getVerticalTextPosition(), b.getHorizontalTextPosition(), viewRect, iconRect, textRect, acceleratorRect, checkIconRect, arrowIconRect, b.getText() == null ? 0 : defaultTextIconGap, defaultTextIconGap ); // Paint the Check if (checkIcon != null) { SynthIcon.paintIcon(checkIcon, context, g, checkIconRect.x, checkIconRect.y, checkIconRect.width, checkIconRect.height); } // Paint the Icon if(b.getIcon() != null) { Icon icon; if(!model.isEnabled()) { icon = (Icon) b.getDisabledIcon(); } else if(model.isPressed() && model.isArmed()) { icon = (Icon) b.getPressedIcon(); if(icon == null) { // Use default icon icon = (Icon) b.getIcon(); } } else { icon = (Icon) b.getIcon(); } if (icon!=null) { SynthIcon.paintIcon(icon, context, g, iconRect.x, iconRect.y, iconRect.width, iconRect.height); } } // Draw the Text if(text != null) { View v = (View) c.getClientProperty(BasicHTML.propertyKey); if (v != null) { v.paint(g, textRect); } else { g.setColor(style.getColor(context, ColorType.TEXT_FOREGROUND)); g.setFont(style.getFont(context)); style.getGraphicsUtils(context).paintText(context, g, text, textRect.x, textRect.y, b.getDisplayedMnemonicIndex()); } } // Draw the Accelerator Text if(acceleratorText != null && !acceleratorText.equals("")) { // Get the maxAccWidth from the parent to calculate the offset. int accOffset = 0; Container parent = b.getParent(); if (parent != null && parent instanceof JPopupMenu) { SynthPopupMenuUI popupUI = (SynthPopupMenuUI) ((JPopupMenu)parent).getUI(); // Note that we can only get here for SynthMenuItemUI // (not SynthMenuUI) since acceleratorText is defined, // so this cast should be safe SynthMenuItemUI miUI = (SynthMenuItemUI) SynthLookAndFeel.getUIOfType(b.getUI(), SynthMenuItemUI.class); if (popupUI != null && miUI != null) { String prop = miUI.getPropertyPrefix() + ".alignAcceleratorText"; boolean align = style.getBoolean(context, prop, true); // Calculate the offset, with which the accelerator texts // will be drawn. if (align) { // When align==true and we're in the LTR case, // we add an offset here so that all accelerators // will be left-justified in their own column. int max = popupUI.getMaxAcceleratorWidth(); if (max > 0) { accOffset = max - acceleratorRect.width; if (!SynthLookAndFeel.isLeftToRight(c)) { // In the RTL, flip the sign so that all // accelerators will be right-justified. accOffset = -accOffset; } } } //else { // Don't need to do anything special here; in the // LTR case, the accelerator is already justified // against the right edge of the menu (and against // the left edge in the RTL case). //} } } SynthStyle accStyle = accContext.getStyle(); g.setColor(accStyle.getColor(accContext, ColorType.TEXT_FOREGROUND)); g.setFont(accStyle.getFont(accContext)); accStyle.getGraphicsUtils(accContext).paintText( accContext, g, acceleratorText, acceleratorRect.x - accOffset, acceleratorRect.y, -1); } // Paint the Arrow if (arrowIcon != null) { SynthIcon.paintIcon(arrowIcon, context, g, arrowIconRect.x, arrowIconRect.y, arrowIconRect.width, arrowIconRect.height); } } /** * Compute and return the location of the icons origin, the * location of origin of the text baseline, and a possibly clipped * version of the compound labels string. Locations are computed * relative to the viewRect rectangle. */ private static String layoutMenuItem( SynthContext context, FontMetrics fm, SynthContext accContext, String text, FontMetrics fmAccel, String acceleratorText, Icon icon, Icon checkIcon, Icon arrowIcon, int verticalAlignment, int horizontalAlignment, int verticalTextPosition, int horizontalTextPosition, Rectangle viewRect, Rectangle iconRect, Rectangle textRect, Rectangle acceleratorRect, Rectangle checkIconRect, Rectangle arrowIconRect, int textIconGap, int menuItemGap ) { // If parent is JPopupMenu, get and store it's UI SynthPopupMenuUI popupUI = null; JComponent b = context.getComponent(); Container parent = b.getParent(); if(parent instanceof JPopupMenu) { popupUI = (SynthPopupMenuUI)SynthLookAndFeel. getUIOfType(((JPopupMenu)parent).getUI(), SynthPopupMenuUI.class); } context.getStyle().getGraphicsUtils(context).layoutText( context, fm, text, icon,horizontalAlignment, verticalAlignment, horizontalTextPosition, verticalTextPosition, viewRect, iconRect, textRect, textIconGap); /* Initialize the acceleratorText bounds rectangle textRect. If a null * or and empty String was specified we substitute "" here * and use 0,0,0,0 for acceleratorTextRect. */ if( (acceleratorText == null) || acceleratorText.equals("") ) { acceleratorRect.width = acceleratorRect.height = 0; acceleratorText = ""; } else { SynthStyle style = accContext.getStyle(); acceleratorRect.width = style.getGraphicsUtils(accContext). computeStringWidth(accContext, fmAccel.getFont(), fmAccel, acceleratorText); acceleratorRect.height = fmAccel.getHeight(); } // Initialize the checkIcon bounds rectangle width & height. if (checkIcon != null) { checkIconRect.width = SynthIcon.getIconWidth(checkIcon, context); checkIconRect.height = SynthIcon.getIconHeight(checkIcon, context); } else { checkIconRect.width = checkIconRect.height = 0; } // Initialize the arrowIcon bounds rectangle width & height. if (arrowIcon != null) { arrowIconRect.width = SynthIcon.getIconWidth(arrowIcon, context); arrowIconRect.height = SynthIcon.getIconHeight(arrowIcon, context); } else { arrowIconRect.width = arrowIconRect.height = 0; } // Note: layoutText() has already left room for // the user icon, so no need to adjust textRect below // to account for the user icon. However, we do have to // reposition textRect when the check icon is visible. Rectangle labelRect = iconRect.union(textRect); if( SynthLookAndFeel.isLeftToRight(context.getComponent()) ) { // Position the check and user icons iconRect.x = viewRect.x; if (checkIcon != null) { checkIconRect.x = viewRect.x; iconRect.x += menuItemGap + checkIconRect.width; textRect.x += menuItemGap + checkIconRect.width; } // Position the arrow icon arrowIconRect.x = viewRect.x + viewRect.width - arrowIconRect.width; // Position the accelerator text rect acceleratorRect.x = viewRect.x + viewRect.width - acceleratorRect.width; /* Align icons and text horizontally */ if(popupUI != null) { int thisTextOffset = popupUI.adjustTextOffset(textRect.x - viewRect.x); textRect.x = thisTextOffset + viewRect.x; if(icon != null) { // REMIND: The following code currently assumes the // default (TRAILING) horizontalTextPosition, which means // it will always place the icon to the left of the text. // Other values of horizontalTextPosition aren't very // useful for menu items, so we ignore them for now, but // someday we might want to fix this situation. int thisIconOffset = popupUI.adjustIconOffset(iconRect.x - viewRect.x); iconRect.x = thisIconOffset + viewRect.x; } } } else { // Position the accelerator text rect acceleratorRect.x = viewRect.x; // Position the arrow icon arrowIconRect.x = viewRect.x; // Position the check and user icons iconRect.x = viewRect.x + viewRect.width - iconRect.width; if (checkIcon != null) { checkIconRect.x = viewRect.x + viewRect.width - checkIconRect.width; textRect.x -= menuItemGap + checkIconRect.width; iconRect.x -= menuItemGap + checkIconRect.width; } /* Align icons and text horizontally */ if(popupUI != null) { int thisTextOffset = viewRect.x + viewRect.width - textRect.x - textRect.width; thisTextOffset = popupUI.adjustTextOffset(thisTextOffset); textRect.x = viewRect.x + viewRect.width - thisTextOffset - textRect.width; if(icon != null) { // REMIND: The following code currently assumes the // default (TRAILING) horizontalTextPosition, which means // it will always place the icon to the right of the text. // Other values of horizontalTextPosition aren't very // useful for menu items, so we ignore them for now, but // someday we might want to fix this situation. int thisIconOffset = viewRect.x + viewRect.width - iconRect.x - iconRect.width; thisIconOffset = popupUI.adjustIconOffset(thisIconOffset); iconRect.x = viewRect.x + viewRect.width - thisIconOffset - iconRect.width; } } } // Align the accelerator text and all icons vertically // with the center of the label rect. int midY = labelRect.y + (labelRect.height/2); iconRect.y = midY - (iconRect.height/2); acceleratorRect.y = midY - (acceleratorRect.height/2); arrowIconRect.y = midY - (arrowIconRect.height/2); checkIconRect.y = midY - (checkIconRect.height/2); return text; } // these rects are used for painting and preferredsize calculations. // they used to be regenerated constantly. Now they are reused. static Rectangle iconRect = new Rectangle(); static Rectangle textRect = new Rectangle(); static Rectangle acceleratorRect = new Rectangle(); static Rectangle checkIconRect = new Rectangle(); static Rectangle arrowIconRect = new Rectangle(); static Rectangle viewRect = new Rectangle(Short.MAX_VALUE,Short.MAX_VALUE); static Rectangle r = new Rectangle(); private static void resetRects() { iconRect.setBounds(0, 0, 0, 0); textRect.setBounds(0, 0, 0, 0); acceleratorRect.setBounds(0, 0, 0, 0); checkIconRect.setBounds(0, 0, 0, 0); arrowIconRect.setBounds(0, 0, 0, 0); viewRect.setBounds(0,0,Short.MAX_VALUE, Short.MAX_VALUE); r.setBounds(0, 0, 0, 0); } protected void installDefaults() { updateStyle(menuItem); } protected void installListeners() { super.installListeners(); menuItem.addPropertyChangeListener(this); } private void updateStyle(JMenuItem mi) { SynthContext context = getContext(mi, ENABLED); SynthStyle oldStyle = style; style = SynthLookAndFeel.updateStyle(context, this); if (oldStyle != style) { String prefix = getPropertyPrefix(); Object value = style.get(context, prefix + ".textIconGap"); if (value != null) { LookAndFeel.installProperty(mi, "iconTextGap", value); } defaultTextIconGap = mi.getIconTextGap(); if (menuItem.getMargin() == null || (menuItem.getMargin() instanceof UIResource)) { Insets insets = (Insets)style.get(context, prefix + ".margin"); if (insets == null) { // Some places assume margins are non-null. insets = SynthLookAndFeel.EMPTY_UIRESOURCE_INSETS; } menuItem.setMargin(insets); } acceleratorDelimiter = style.getString(context, prefix + ".acceleratorDelimiter", "+"); arrowIcon = style.getIcon(context, prefix + ".arrowIcon"); checkIcon = style.getIcon(context, prefix + ".checkIcon"); if (oldStyle != null) { uninstallKeyboardActions(); installKeyboardActions(); } } context.dispose(); SynthContext accContext = getContext(mi, Region.MENU_ITEM_ACCELERATOR, ENABLED); accStyle = SynthLookAndFeel.updateStyle(accContext, this); accContext.dispose(); } protected void uninstallDefaults() { SynthContext context = getContext(menuItem, ENABLED); style.uninstallDefaults(context); context.dispose(); style = null; SynthContext accContext = getContext(menuItem, Region.MENU_ITEM_ACCELERATOR, ENABLED); accStyle.uninstallDefaults(accContext); accContext.dispose(); accStyle = null; super.uninstallDefaults(); } protected void uninstallListeners() { super.uninstallListeners(); menuItem.removePropertyChangeListener(this); } public SynthContext getContext(JComponent c) { return getContext(c, getComponentState(c)); } SynthContext getContext(JComponent c, int state) { return SynthContext.getContext(SynthContext.class, c, SynthLookAndFeel.getRegion(c), style, state); } public SynthContext getContext(JComponent c, Region region) { return getContext(c, region, getComponentState(c, region)); } private SynthContext getContext(JComponent c, Region region, int state) { return SynthContext.getContext(SynthContext.class, c, region, accStyle, state); } private Region getRegion(JComponent c) { return SynthLookAndFeel.getRegion(c); } private int getComponentState(JComponent c) { int state; if (!c.isEnabled()) { state = DISABLED; } else if (menuItem.isArmed()) { state = MOUSE_OVER; } else { state = SynthLookAndFeel.getComponentState(c); } if (menuItem.isSelected()) { state |= SELECTED; } return state; } private int getComponentState(JComponent c, Region region) { return getComponentState(c); } protected Dimension getPreferredMenuItemSize(JComponent c, Icon checkIcon, Icon arrowIcon, int defaultTextIconGap) { SynthContext context = getContext(c); SynthContext accContext = getContext(c, Region.MENU_ITEM_ACCELERATOR); Dimension value = getPreferredMenuItemSize(context, accContext, c, checkIcon, arrowIcon, defaultTextIconGap, acceleratorDelimiter); context.dispose(); accContext.dispose(); return value; } public void update(Graphics g, JComponent c) { SynthContext context = getContext(c); SynthLookAndFeel.update(context, g); paintBackground(context, g, c); paint(context, g); context.dispose(); } public void paint(Graphics g, JComponent c) { SynthContext context = getContext(c); paint(context, g); context.dispose(); } protected void paint(SynthContext context, Graphics g) { SynthContext accContext = getContext(menuItem, Region.MENU_ITEM_ACCELERATOR); // Refetch the appropriate check indicator for the current state String prefix = getPropertyPrefix(); Icon checkIcon = style.getIcon(context, prefix + ".checkIcon"); Icon arrowIcon = style.getIcon(context, prefix + ".arrowIcon"); paint(context, accContext, g, checkIcon, arrowIcon, acceleratorDelimiter, defaultTextIconGap); accContext.dispose(); } void paintBackground(SynthContext context, Graphics g, JComponent c) { context.getPainter().paintMenuItemBackground(context, g, 0, 0, c.getWidth(), c.getHeight()); } public void paintBorder(SynthContext context, Graphics g, int x, int y, int w, int h) { context.getPainter().paintMenuItemBorder(context, g, x, y, w, h); } public void propertyChange(PropertyChangeEvent e) { if (SynthLookAndFeel.shouldUpdateStyle(e)) { updateStyle((JMenuItem)e.getSource()); } } }