Path: blob/master/src/java.desktop/macosx/classes/com/apple/laf/AquaMenuPainter.java
41154 views
/*1* Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package com.apple.laf;2627import java.awt.*;28import java.awt.event.*;2930import javax.swing.*;31import javax.swing.border.Border;32import javax.swing.plaf.basic.BasicHTML;33import javax.swing.text.View;3435import sun.swing.SwingUtilities2;3637import apple.laf.JRSUIConstants.*;3839import com.apple.laf.AquaIcon.InvertableIcon;40import com.apple.laf.AquaUtils.RecyclableSingleton;41import com.apple.laf.AquaUtils.RecyclableSingletonFromDefaultConstructor;4243/**44* AquaMenuPainter, implements paintMenuItem to avoid code duplication45*46* BasicMenuItemUI didn't factor out the various parts of the Menu, and47* we subclass it and its subclasses BasicMenuUI48* Our classes need an implementation of paintMenuItem49* that allows them to paint their own backgrounds50*/5152public class AquaMenuPainter {53// Glyph statics:54// ASCII character codes55static final byte56kShiftGlyph = 0x05,57kOptionGlyph = 0x07,58kControlGlyph = 0x06,59kPencilGlyph = 0x0F,60kCommandMark = 0x11;6162// Unicode character codes63static final char64kUBlackDiamond = 0x25C6,65kUCheckMark = 0x2713,66kUControlGlyph = 0x2303,67kUOptionGlyph = 0x2325,68kUEnterGlyph = 0x2324,69kUCommandGlyph = 0x2318,70kULeftDeleteGlyph = 0x232B,71kURightDeleteGlyph = 0x2326,72kUShiftGlyph = 0x21E7,73kUCapsLockGlyph = 0x21EA;7475static final int ALT_GRAPH_MASK = 1 << 5; // New to Java276@SuppressWarnings("deprecation")77static final int sUnsupportedModifiersMask =78~(InputEvent.CTRL_MASK | InputEvent.ALT_MASK | InputEvent.SHIFT_MASK79| InputEvent.META_MASK | ALT_GRAPH_MASK);8081interface Client {82public void paintBackground(Graphics g, JComponent c, int menuWidth, int menuHeight);83}8485// Return a string with the proper modifier glyphs86static String getKeyModifiersText(final int modifiers, final boolean isLeftToRight) {87return getKeyModifiersUnicode(modifiers, isLeftToRight);88}8990// Return a string with the proper modifier glyphs91@SuppressWarnings("deprecation")92private static String getKeyModifiersUnicode(final int modifiers, final boolean isLeftToRight) {93final StringBuilder buf = new StringBuilder(2);94// Order (from StandardMenuDef.c): control, option(alt), shift, cmd95// reverse for right-to-left96//$ check for substitute key glyphs for localization97if (isLeftToRight) {98if ((modifiers & InputEvent.CTRL_MASK) != 0) {99buf.append(kUControlGlyph);100}101if ((modifiers & (InputEvent.ALT_MASK | ALT_GRAPH_MASK)) != 0) {102buf.append(kUOptionGlyph);103}104if ((modifiers & InputEvent.SHIFT_MASK) != 0) {105buf.append(kUShiftGlyph);106}107if ((modifiers & InputEvent.META_MASK) != 0) {108buf.append(kUCommandGlyph);109}110} else {111if ((modifiers & InputEvent.META_MASK) != 0) {112buf.append(kUCommandGlyph);113}114if ((modifiers & InputEvent.SHIFT_MASK) != 0) {115buf.append(kUShiftGlyph);116}117if ((modifiers & (InputEvent.ALT_MASK | ALT_GRAPH_MASK)) != 0) {118buf.append(kUOptionGlyph);119}120if ((modifiers & InputEvent.CTRL_MASK) != 0) {121buf.append(kUControlGlyph);122}123}124return buf.toString();125}126127private static final RecyclableSingleton<AquaMenuPainter> sPainter = new RecyclableSingletonFromDefaultConstructor<AquaMenuPainter>(AquaMenuPainter.class);128static AquaMenuPainter instance() {129return sPainter.get();130}131132static final int defaultMenuItemGap = 2;133static final int kAcceleratorArrowSpace = 16; // Accel space doesn't overlap arrow space, even though items can't have both134135static class RecyclableBorder extends RecyclableSingleton<Border> {136final String borderName;137RecyclableBorder(final String borderName) { this.borderName = borderName; }138protected Border getInstance() { return UIManager.getBorder(borderName); }139}140141private static final RecyclableBorder menuBarPainter = new RecyclableBorder("MenuBar.backgroundPainter");142private static final RecyclableBorder selectedMenuBarItemPainter = new RecyclableBorder("MenuBar.selectedBackgroundPainter");143private static final RecyclableBorder selectedMenuItemPainter = new RecyclableBorder("MenuItem.selectedBackgroundPainter");144145public void paintMenuBarBackground(final Graphics g, final int width, final int height, final JComponent c) {146g.setColor(c == null ? Color.white : c.getBackground());147g.fillRect(0, 0, width, height);148menuBarPainter.get().paintBorder(null, g, 0, 0, width, height);149}150151public void paintSelectedMenuTitleBackground(final Graphics g, final int width, final int height) {152selectedMenuBarItemPainter.get().paintBorder(null, g, -1, 0, width + 2, height);153}154155public void paintSelectedMenuItemBackground(final Graphics g, final int width, final int height) {156selectedMenuItemPainter.get().paintBorder(null, g, 0, 0, width, height);157}158159protected void paintMenuItem(final Client client, final Graphics g, final JComponent c, final Icon checkIcon, final Icon arrowIcon, final Color background, final Color foreground, final Color disabledForeground, final Color selectionForeground, final int defaultTextIconGap, final Font acceleratorFont) {160final JMenuItem b = (JMenuItem)c;161final ButtonModel model = b.getModel();162163// Dimension size = b.getSize();164final int menuWidth = b.getWidth();165final int menuHeight = b.getHeight();166final Insets i = c.getInsets();167168Rectangle viewRect = new Rectangle(0, 0, menuWidth, menuHeight);169170viewRect.x += i.left;171viewRect.y += i.top;172viewRect.width -= (i.right + viewRect.x);173viewRect.height -= (i.bottom + viewRect.y);174175final Font holdf = g.getFont();176final Color holdc = g.getColor();177final Font f = c.getFont();178g.setFont(f);179final FontMetrics fm = g.getFontMetrics(f);180181final FontMetrics fmAccel = g.getFontMetrics(acceleratorFont);182183// Paint background (doesn't touch the Graphics object's color)184if (c.isOpaque()) {185client.paintBackground(g, c, menuWidth, menuHeight);186}187188// get Accelerator text189final KeyStroke accelerator = b.getAccelerator();190String modifiersString = "", keyString = "";191final boolean leftToRight = AquaUtils.isLeftToRight(c);192if (accelerator != null) {193final int modifiers = accelerator.getModifiers();194if (modifiers > 0) {195modifiersString = getKeyModifiersText(modifiers, leftToRight);196}197final int keyCode = accelerator.getKeyCode();198if (keyCode != 0) {199keyString = KeyEvent.getKeyText(keyCode);200} else {201keyString += accelerator.getKeyChar();202}203}204205Rectangle iconRect = new Rectangle();206Rectangle textRect = new Rectangle();207Rectangle acceleratorRect = new Rectangle();208Rectangle checkIconRect = new Rectangle();209Rectangle arrowIconRect = new Rectangle();210211// layout the text and icon212final String text = layoutMenuItem(b, fm, b.getText(), fmAccel, keyString, modifiersString, b.getIcon(), checkIcon, arrowIcon, b.getVerticalAlignment(), b.getHorizontalAlignment(), b.getVerticalTextPosition(), b.getHorizontalTextPosition(), viewRect, iconRect, textRect, acceleratorRect, checkIconRect, arrowIconRect, b.getText() == null ? 0 : defaultTextIconGap, defaultTextIconGap);213214// if this is in a AquaScreenMenuBar that's attached to a DialogPeer215// the native menu will be disabled, though the awt Menu won't know about it216// so the JPopupMenu will not have visibility set and the items should draw disabled217// If it's not on a JPopupMenu then it should just use the model's enable state218final Container parent = b.getParent();219final boolean parentIsMenuBar = parent instanceof JMenuBar;220221Container ancestor = parent;222while (ancestor != null && !(ancestor instanceof JPopupMenu)) ancestor = ancestor.getParent();223224boolean isEnabled = model.isEnabled() && (ancestor == null || ancestor.isVisible());225226// Set the accel/normal text color227boolean isSelected = false;228if (!isEnabled) {229// *** paint the text disabled230g.setColor(disabledForeground);231} else {232// *** paint the text normally233if (model.isArmed() || (c instanceof JMenu && model.isSelected())) {234g.setColor(selectionForeground);235isSelected = true;236} else {237g.setColor(parentIsMenuBar ? parent.getForeground() : b.getForeground()); // Which is either MenuItem.foreground or the user's choice238}239}240241// We want to paint the icon after the text color is set since some icon painting depends on the correct242// graphics color being set243// See <rdar://problem/3792383> Menu icons missing in Java2D's Lines.Joins demo244// Paint the Icon245if (b.getIcon() != null) {246paintIcon(g, b, iconRect, isEnabled);247}248249// Paint the Check using the current text color250if (checkIcon != null) {251paintCheck(g, b, checkIcon, checkIconRect);252}253254// Draw the accelerator first in case the HTML renderer changes the color255if (keyString != null && !keyString.isEmpty()) {256final int yAccel = acceleratorRect.y + fm.getAscent();257if (modifiersString.isEmpty()) {258// just draw the keyString259SwingUtilities2.drawString(c, g, keyString, acceleratorRect.x, yAccel);260} else {261final int modifiers = accelerator.getModifiers();262int underlinedChar = 0;263if ((modifiers & ALT_GRAPH_MASK) > 0) underlinedChar = kUOptionGlyph; // This is a Java2 thing, we won't be getting kOptionGlyph264// The keyStrings should all line up, so always adjust the width by the same amount265// (if they're multi-char, they won't line up but at least they won't be cut off)266final int emWidth = Math.max(fm.charWidth('M'), SwingUtilities.computeStringWidth(fm, keyString));267268if (leftToRight) {269g.setFont(acceleratorFont);270drawString(g, c, modifiersString, underlinedChar, acceleratorRect.x, yAccel, isEnabled, isSelected);271g.setFont(f);272SwingUtilities2.drawString(c, g, keyString, acceleratorRect.x + acceleratorRect.width - emWidth, yAccel);273} else {274final int xAccel = acceleratorRect.x + emWidth;275g.setFont(acceleratorFont);276drawString(g, c, modifiersString, underlinedChar, xAccel, yAccel, isEnabled, isSelected);277g.setFont(f);278SwingUtilities2.drawString(c, g, keyString, xAccel - fm.stringWidth(keyString), yAccel);279}280}281}282283// Draw the Text284if (text != null && !text.isEmpty()) {285final View v = (View)c.getClientProperty(BasicHTML.propertyKey);286if (v != null) {287v.paint(g, textRect);288} else {289final int mnemonic = (AquaMnemonicHandler.isMnemonicHidden() ? -1 : model.getMnemonic());290drawString(g, c, text, mnemonic, textRect.x, textRect.y + fm.getAscent(), isEnabled, isSelected);291}292}293294// Paint the Arrow295if (arrowIcon != null) {296paintArrow(g, b, model, arrowIcon, arrowIconRect);297}298299g.setColor(holdc);300g.setFont(holdf);301}302303// All this had to be copied from BasicMenuItemUI, just to get the right keyModifiersText fn304// and a few Mac tweaks305protected Dimension getPreferredMenuItemSize(final JComponent c, final Icon checkIcon, final Icon arrowIcon, final int defaultTextIconGap, final Font acceleratorFont) {306final JMenuItem b = (JMenuItem)c;307final Icon icon = b.getIcon();308final String text = b.getText();309final KeyStroke accelerator = b.getAccelerator();310String keyString = "", modifiersString = "";311312if (accelerator != null) {313final int modifiers = accelerator.getModifiers();314if (modifiers > 0) {315modifiersString = getKeyModifiersText(modifiers, true); // doesn't matter, this is just for metrics316}317final int keyCode = accelerator.getKeyCode();318if (keyCode != 0) {319keyString = KeyEvent.getKeyText(keyCode);320} else {321keyString += accelerator.getKeyChar();322}323}324325final Font font = b.getFont();326final FontMetrics fm = b.getFontMetrics(font);327final FontMetrics fmAccel = b.getFontMetrics(acceleratorFont);328329Rectangle iconRect = new Rectangle();330Rectangle textRect = new Rectangle();331Rectangle acceleratorRect = new Rectangle();332Rectangle checkIconRect = new Rectangle();333Rectangle arrowIconRect = new Rectangle();334Rectangle viewRect = new Rectangle(Short.MAX_VALUE, Short.MAX_VALUE);335336layoutMenuItem(b, fm, text, fmAccel, keyString, modifiersString, icon, checkIcon, arrowIcon, b.getVerticalAlignment(), b.getHorizontalAlignment(), b.getVerticalTextPosition(), b.getHorizontalTextPosition(), viewRect, iconRect, textRect, acceleratorRect, checkIconRect, arrowIconRect, text == null ? 0 : defaultTextIconGap, defaultTextIconGap);337// find the union of the icon and text rects338Rectangle r = new Rectangle();339r.setBounds(textRect);340r = SwingUtilities.computeUnion(iconRect.x, iconRect.y, iconRect.width, iconRect.height, r);341// r = iconRect.union(textRect);342343// Add in the accelerator344boolean acceleratorTextIsEmpty = (keyString == null) || keyString.isEmpty();345346if (!acceleratorTextIsEmpty) {347r.width += acceleratorRect.width;348}349350if (!isTopLevelMenu(b)) {351// Add in the checkIcon352r.width += checkIconRect.width;353r.width += defaultTextIconGap;354355// Add in the arrowIcon space356r.width += defaultTextIconGap;357r.width += arrowIconRect.width;358}359360final Insets insets = b.getInsets();361if (insets != null) {362r.width += insets.left + insets.right;363r.height += insets.top + insets.bottom;364}365366// Tweak for Mac367r.width += 4 + defaultTextIconGap;368r.height = Math.max(r.height, 18);369370return r.getSize();371}372373protected void paintCheck(final Graphics g, final JMenuItem item, Icon checkIcon, Rectangle checkIconRect) {374if (isTopLevelMenu(item) || !item.isSelected()) return;375376if (item.isArmed() && checkIcon instanceof InvertableIcon) {377((InvertableIcon)checkIcon).getInvertedIcon().paintIcon(item, g, checkIconRect.x, checkIconRect.y);378} else {379checkIcon.paintIcon(item, g, checkIconRect.x, checkIconRect.y);380}381}382383protected void paintIcon(final Graphics g, final JMenuItem c, final Rectangle localIconRect, boolean isEnabled) {384final ButtonModel model = c.getModel();385Icon icon;386if (!isEnabled) {387icon = c.getDisabledIcon();388} else if (model.isPressed() && model.isArmed()) {389icon = c.getPressedIcon();390if (icon == null) {391// Use default icon392icon = c.getIcon();393}394} else {395icon = c.getIcon();396}397398if (icon != null) icon.paintIcon(c, g, localIconRect.x, localIconRect.y);399}400401protected void paintArrow(Graphics g, JMenuItem c, ButtonModel model, Icon arrowIcon, Rectangle arrowIconRect) {402if (isTopLevelMenu(c)) return;403404if (c instanceof JMenu && (model.isArmed() || model.isSelected()) && arrowIcon instanceof InvertableIcon) {405((InvertableIcon)arrowIcon).getInvertedIcon().paintIcon(c, g, arrowIconRect.x, arrowIconRect.y);406} else {407arrowIcon.paintIcon(c, g, arrowIconRect.x, arrowIconRect.y);408}409}410411/** Draw a string with the graphics g at location (x,y) just like g.drawString() would.412* The first occurrence of underlineChar in text will be underlined. The matching is413* not case sensitive.414*/415public void drawString(final Graphics g, final JComponent c, final String text, final int underlinedChar, final int x, final int y, final boolean isEnabled, final boolean isSelected) {416char lc, uc;417int index = -1, lci, uci;418419if (underlinedChar != '\0') {420uc = Character.toUpperCase((char)underlinedChar);421lc = Character.toLowerCase((char)underlinedChar);422423uci = text.indexOf(uc);424lci = text.indexOf(lc);425426if (uci == -1) index = lci;427else if (lci == -1) index = uci;428else index = (lci < uci) ? lci : uci;429}430431SwingUtilities2.drawStringUnderlineCharAt(c, g, text, index, x, y);432}433434/*435* Returns false if the component is a JMenu and it is a top436* level menu (on the menubar).437*/438private static boolean isTopLevelMenu(final JMenuItem menuItem) {439return (menuItem instanceof JMenu) && (((JMenu)menuItem).isTopLevelMenu());440}441442private String layoutMenuItem(final JMenuItem menuItem, final FontMetrics fm, final String text, final FontMetrics fmAccel, String keyString, final String modifiersString, final Icon icon, final Icon checkIcon, final Icon arrowIcon, final int verticalAlignment, final int horizontalAlignment, final int verticalTextPosition, final int horizontalTextPosition, final Rectangle viewR, final Rectangle iconR, final Rectangle textR, final Rectangle acceleratorR, final Rectangle checkIconR, final Rectangle arrowIconR, final int textIconGap, final int menuItemGap) {443// Force it to do "LEFT", then flip the rects if we're right-to-left444SwingUtilities.layoutCompoundLabel(menuItem, fm, text, icon, verticalAlignment, SwingConstants.LEFT, verticalTextPosition, horizontalTextPosition, viewR, iconR, textR, textIconGap);445446final boolean acceleratorTextIsEmpty = (keyString == null) || keyString.isEmpty();447448if (acceleratorTextIsEmpty) {449acceleratorR.width = acceleratorR.height = 0;450keyString = "";451} else {452// Accel space doesn't overlap arrow space, even though items can't have both453acceleratorR.width = SwingUtilities.computeStringWidth(fmAccel, modifiersString);454// The keyStrings should all line up, so always adjust the width by the same amount455// (if they're multi-char, they won't line up but at least they won't be cut off)456acceleratorR.width += Math.max(fm.charWidth('M'), SwingUtilities.computeStringWidth(fm, keyString));457acceleratorR.height = fmAccel.getHeight();458}459460/* Initialize the checkIcon bounds rectangle checkIconR.461*/462463final boolean isTopLevelMenu = isTopLevelMenu(menuItem);464if (!isTopLevelMenu) {465if (checkIcon != null) {466checkIconR.width = checkIcon.getIconWidth();467checkIconR.height = checkIcon.getIconHeight();468} else {469checkIconR.width = checkIconR.height = 16;470}471472/* Initialize the arrowIcon bounds rectangle arrowIconR.473*/474475if (arrowIcon != null) {476arrowIconR.width = arrowIcon.getIconWidth();477arrowIconR.height = arrowIcon.getIconHeight();478} else {479arrowIconR.width = arrowIconR.height = 16;480}481482textR.x += 12;483iconR.x += 12;484}485486final Rectangle labelR = iconR.union(textR);487488// Position the Accelerator text rect489// Menu shortcut text *ought* to have the letters left-justified - look at a menu with an "M" in it490acceleratorR.x += (viewR.width - arrowIconR.width - acceleratorR.width);491acceleratorR.y = viewR.y + (viewR.height / 2) - (acceleratorR.height / 2);492493if (!isTopLevelMenu) {494// if ( GetSysDirection() < 0 ) hierRect.right = hierRect.left + w + 4;495// else hierRect.left = hierRect.right - w - 4;496arrowIconR.x = (viewR.width - arrowIconR.width) + 1;497arrowIconR.y = viewR.y + (labelR.height / 2) - (arrowIconR.height / 2) + 1;498499checkIconR.y = viewR.y + (labelR.height / 2) - (checkIconR.height / 2);500checkIconR.x = 5;501502textR.width += 8;503}504505/*System.out.println("Layout: " +horizontalAlignment+ " v=" +viewR+" c="+checkIconR+" i="+506iconR+" t="+textR+" acc="+acceleratorR+" a="+arrowIconR);*/507508if (!AquaUtils.isLeftToRight(menuItem)) {509// Flip the rectangles so that instead of [check][icon][text][accel/arrow] it's [accel/arrow][text][icon][check]510final int w = viewR.width;511checkIconR.x = w - (checkIconR.x + checkIconR.width);512iconR.x = w - (iconR.x + iconR.width);513textR.x = w - (textR.x + textR.width);514acceleratorR.x = w - (acceleratorR.x + acceleratorR.width);515arrowIconR.x = w - (arrowIconR.x + arrowIconR.width);516}517textR.x += menuItemGap;518iconR.x += menuItemGap;519520return text;521}522523public static Border getMenuBarPainter() {524final AquaBorder border = new AquaBorder.Default();525border.painter.state.set(Widget.MENU_BAR);526return border;527}528529public static Border getSelectedMenuBarItemPainter() {530final AquaBorder border = new AquaBorder.Default();531border.painter.state.set(Widget.MENU_TITLE);532border.painter.state.set(State.PRESSED);533return border;534}535536public static Border getSelectedMenuItemPainter() {537final AquaBorder border = new AquaBorder.Default();538border.painter.state.set(Widget.MENU_ITEM);539border.painter.state.set(State.PRESSED);540return border;541}542}543544545