1 /* 2 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.apple.laf; 27 28 import java.awt.*; 29 import java.awt.event.*; 30 31 import javax.swing.*; 32 import javax.swing.border.Border; 33 import javax.swing.plaf.basic.BasicHTML; 34 import javax.swing.text.View; 35 36 import sun.swing.SwingUtilities2; 37 38 import apple.laf.JRSUIConstants.*; 39 40 import com.apple.laf.AquaIcon.InvertableIcon; 41 import com.apple.laf.AquaUtils.RecyclableSingleton; 42 import com.apple.laf.AquaUtils.RecyclableSingletonFromDefaultConstructor; 43 44 /** 45 * AquaMenuPainter, implements paintMenuItem to avoid code duplication 46 * 47 * BasicMenuItemUI didn't factor out the various parts of the Menu, and 48 * we subclass it and its subclasses BasicMenuUI 49 * Our classes need an implementation of paintMenuItem 50 * that allows them to paint their own backgrounds 51 */ 52 53 public class AquaMenuPainter { 54 // Glyph statics: 55 // ASCII character codes 56 static final byte 57 kShiftGlyph = 0x05, 58 kOptionGlyph = 0x07, 59 kControlGlyph = 0x06, 60 kPencilGlyph = 0x0F, 61 kCommandMark = 0x11; 62 63 // Unicode character codes 64 static final char 65 kUBlackDiamond = 0x25C6, 66 kUCheckMark = 0x2713, 67 kUControlGlyph = 0x2303, 68 kUOptionGlyph = 0x2325, 69 kUEnterGlyph = 0x2324, 70 kUCommandGlyph = 0x2318, 71 kULeftDeleteGlyph = 0x232B, 72 kURightDeleteGlyph = 0x2326, 73 kUShiftGlyph = 0x21E7, 74 kUCapsLockGlyph = 0x21EA; 75 76 static final int ALT_GRAPH_MASK = 1 << 5; // New to Java2 77 static final int sUnsupportedModifiersMask = ~(InputEvent.CTRL_MASK | InputEvent.ALT_MASK | InputEvent.SHIFT_MASK | InputEvent.META_MASK | ALT_GRAPH_MASK); 78 79 interface Client { 80 public void paintBackground(Graphics g, JComponent c, int menuWidth, int menuHeight); 81 } 82 83 // Return a string with the proper modifier glyphs 84 static String getKeyModifiersText(final int modifiers, final boolean isLeftToRight) { 85 return getKeyModifiersUnicode(modifiers, isLeftToRight); 86 } 87 88 // Return a string with the proper modifier glyphs 89 private static String getKeyModifiersUnicode(final int modifiers, final boolean isLeftToRight) { 90 final StringBuilder buf = new StringBuilder(2); 91 // Order (from StandardMenuDef.c): control, option(alt), shift, cmd 92 // reverse for right-to-left 93 //$ check for substitute key glyphs for localization 94 if (isLeftToRight) { 95 if ((modifiers & InputEvent.CTRL_MASK) != 0) { 96 buf.append(kUControlGlyph); 97 } 98 if ((modifiers & (InputEvent.ALT_MASK | ALT_GRAPH_MASK)) != 0) { 99 buf.append(kUOptionGlyph); 100 } 101 if ((modifiers & InputEvent.SHIFT_MASK) != 0) { 102 buf.append(kUShiftGlyph); 103 } 104 if ((modifiers & InputEvent.META_MASK) != 0) { 105 buf.append(kUCommandGlyph); 106 } 107 } else { 108 if ((modifiers & InputEvent.META_MASK) != 0) { 109 buf.append(kUCommandGlyph); 110 } 111 if ((modifiers & InputEvent.SHIFT_MASK) != 0) { 112 buf.append(kUShiftGlyph); 113 } 114 if ((modifiers & (InputEvent.ALT_MASK | ALT_GRAPH_MASK)) != 0) { 115 buf.append(kUOptionGlyph); 116 } 117 if ((modifiers & InputEvent.CTRL_MASK) != 0) { 118 buf.append(kUControlGlyph); 119 } 120 } 121 return buf.toString(); 122 } 123 124 static final RecyclableSingleton<AquaMenuPainter> sPainter = new RecyclableSingletonFromDefaultConstructor<AquaMenuPainter>(AquaMenuPainter.class); 125 static AquaMenuPainter instance() { 126 return sPainter.get(); 127 } 128 129 static final int defaultMenuItemGap = 2; 130 static final int kAcceleratorArrowSpace = 16; // Accel space doesn't overlap arrow space, even though items can't have both 131 132 static class RecyclableBorder extends RecyclableSingleton<Border> { 133 final String borderName; 134 RecyclableBorder(final String borderName) { this.borderName = borderName; } 135 protected Border getInstance() { return UIManager.getBorder(borderName); } 136 } 137 138 protected final RecyclableBorder menuBarPainter = new RecyclableBorder("MenuBar.backgroundPainter"); 139 protected final RecyclableBorder selectedMenuBarItemPainter = new RecyclableBorder("MenuBar.selectedBackgroundPainter"); 140 protected final RecyclableBorder selectedMenuItemPainter = new RecyclableBorder("MenuItem.selectedBackgroundPainter"); 141 142 public void paintMenuBarBackground(final Graphics g, final int width, final int height, final JComponent c) { 143 g.setColor(c == null ? Color.white : c.getBackground()); 144 g.fillRect(0, 0, width, height); 145 menuBarPainter.get().paintBorder(null, g, 0, 0, width, height); 146 } 147 148 public void paintSelectedMenuTitleBackground(final Graphics g, final int width, final int height) { 149 selectedMenuBarItemPainter.get().paintBorder(null, g, -1, 0, width + 2, height); 150 } 151 152 public void paintSelectedMenuItemBackground(final Graphics g, final int width, final int height) { 153 selectedMenuItemPainter.get().paintBorder(null, g, 0, 0, width, height); 154 } 155 156 protected 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) { 157 final JMenuItem b = (JMenuItem)c; 158 final ButtonModel model = b.getModel(); 159 160 // Dimension size = b.getSize(); 161 final int menuWidth = b.getWidth(); 162 final int menuHeight = b.getHeight(); 163 final Insets i = c.getInsets(); 164 165 Rectangle viewRect = new Rectangle(0, 0, menuWidth, menuHeight); 166 167 viewRect.x += i.left; 168 viewRect.y += i.top; 169 viewRect.width -= (i.right + viewRect.x); 170 viewRect.height -= (i.bottom + viewRect.y); 171 172 final Font holdf = g.getFont(); 173 final Color holdc = g.getColor(); 174 final Font f = c.getFont(); 175 g.setFont(f); 176 final FontMetrics fm = g.getFontMetrics(f); 177 178 final FontMetrics fmAccel = g.getFontMetrics(acceleratorFont); 179 180 // Paint background (doesn't touch the Graphics object's color) 181 if (c.isOpaque()) { 182 client.paintBackground(g, c, menuWidth, menuHeight); 183 } 184 185 // get Accelerator text 186 final KeyStroke accelerator = b.getAccelerator(); 187 String modifiersString = "", keyString = ""; 188 final boolean leftToRight = AquaUtils.isLeftToRight(c); 189 if (accelerator != null) { 190 final int modifiers = accelerator.getModifiers(); 191 if (modifiers > 0) { 192 modifiersString = getKeyModifiersText(modifiers, leftToRight); 193 } 194 final int keyCode = accelerator.getKeyCode(); 195 if (keyCode != 0) { 196 keyString = KeyEvent.getKeyText(keyCode); 197 } else { 198 keyString += accelerator.getKeyChar(); 199 } 200 } 201 202 Rectangle iconRect = new Rectangle(); 203 Rectangle textRect = new Rectangle(); 204 Rectangle acceleratorRect = new Rectangle(); 205 Rectangle checkIconRect = new Rectangle(); 206 Rectangle arrowIconRect = new Rectangle(); 207 208 // layout the text and icon 209 final 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); 210 211 // if this is in a AquaScreenMenuBar that's attached to a DialogPeer 212 // the native menu will be disabled, though the awt Menu won't know about it 213 // so the JPopupMenu will not have visibility set and the items should draw disabled 214 // If it's not on a JPopupMenu then it should just use the model's enable state 215 final Container parent = b.getParent(); 216 final boolean parentIsMenuBar = parent instanceof JMenuBar; 217 218 Container ancestor = parent; 219 while (ancestor != null && !(ancestor instanceof JPopupMenu)) ancestor = ancestor.getParent(); 220 221 boolean isEnabled = model.isEnabled() && (ancestor == null || ancestor.isVisible()); 222 223 // Set the accel/normal text color 224 boolean isSelected = false; 225 if (!isEnabled) { 226 // *** paint the text disabled 227 g.setColor(disabledForeground); 228 } else { 229 // *** paint the text normally 230 if (model.isArmed() || (c instanceof JMenu && model.isSelected())) { 231 g.setColor(selectionForeground); 232 isSelected = true; 233 } else { 234 g.setColor(parentIsMenuBar ? parent.getForeground() : b.getForeground()); // Which is either MenuItem.foreground or the user's choice 235 } 236 } 237 238 // We want to paint the icon after the text color is set since some icon painting depends on the correct 239 // graphics color being set 240 // See <rdar://problem/3792383> Menu icons missing in Java2D's Lines.Joins demo 241 // Paint the Icon 242 if (b.getIcon() != null) { 243 paintIcon(g, b, iconRect, isEnabled); 244 } 245 246 // Paint the Check using the current text color 247 if (checkIcon != null) { 248 paintCheck(g, b, checkIcon, checkIconRect); 249 } 250 251 // Draw the accelerator first in case the HTML renderer changes the color 252 if (keyString != null && !keyString.equals("")) { 253 final int yAccel = acceleratorRect.y + fm.getAscent(); 254 if (modifiersString.equals("")) { 255 // just draw the keyString 256 SwingUtilities2.drawString(c, g, keyString, acceleratorRect.x, yAccel); 257 } else { 258 final int modifiers = accelerator.getModifiers(); 259 int underlinedChar = 0; 260 if ((modifiers & ALT_GRAPH_MASK) > 0) underlinedChar = kUOptionGlyph; // This is a Java2 thing, we won't be getting kOptionGlyph 261 // The keyStrings should all line up, so always adjust the width by the same amount 262 // (if they're multi-char, they won't line up but at least they won't be cut off) 263 final int emWidth = Math.max(fm.charWidth('M'), SwingUtilities.computeStringWidth(fm, keyString)); 264 265 if (leftToRight) { 266 g.setFont(acceleratorFont); 267 drawString(g, c, modifiersString, underlinedChar, acceleratorRect.x, yAccel, isEnabled, isSelected); 268 g.setFont(f); 269 SwingUtilities2.drawString(c, g, keyString, acceleratorRect.x + acceleratorRect.width - emWidth, yAccel); 270 } else { 271 final int xAccel = acceleratorRect.x + emWidth; 272 g.setFont(acceleratorFont); 273 drawString(g, c, modifiersString, underlinedChar, xAccel, yAccel, isEnabled, isSelected); 274 g.setFont(f); 275 SwingUtilities2.drawString(c, g, keyString, xAccel - fm.stringWidth(keyString), yAccel); 276 } 277 } 278 } 279 280 // Draw the Text 281 if (text != null && !text.equals("")) { 282 final View v = (View)c.getClientProperty(BasicHTML.propertyKey); 283 if (v != null) { 284 v.paint(g, textRect); 285 } else { 286 final int mnemonic = (AquaMnemonicHandler.isMnemonicHidden() ? -1 : model.getMnemonic()); 287 drawString(g, c, text, mnemonic, textRect.x, textRect.y + fm.getAscent(), isEnabled, isSelected); 288 } 289 } 290 291 // Paint the Arrow 292 if (arrowIcon != null) { 293 paintArrow(g, b, model, arrowIcon, arrowIconRect); 294 } 295 296 g.setColor(holdc); 297 g.setFont(holdf); 298 } 299 300 // All this had to be copied from BasicMenuItemUI, just to get the right keyModifiersText fn 301 // and a few Mac tweaks 302 protected Dimension getPreferredMenuItemSize(final JComponent c, final Icon checkIcon, final Icon arrowIcon, final int defaultTextIconGap, final Font acceleratorFont) { 303 final JMenuItem b = (JMenuItem)c; 304 final Icon icon = b.getIcon(); 305 final String text = b.getText(); 306 final KeyStroke accelerator = b.getAccelerator(); 307 String keyString = "", modifiersString = ""; 308 309 if (accelerator != null) { 310 final int modifiers = accelerator.getModifiers(); 311 if (modifiers > 0) { 312 modifiersString = getKeyModifiersText(modifiers, true); // doesn't matter, this is just for metrics 313 } 314 final int keyCode = accelerator.getKeyCode(); 315 if (keyCode != 0) { 316 keyString = KeyEvent.getKeyText(keyCode); 317 } else { 318 keyString += accelerator.getKeyChar(); 319 } 320 } 321 322 final Font font = b.getFont(); 323 final FontMetrics fm = b.getFontMetrics(font); 324 final FontMetrics fmAccel = b.getFontMetrics(acceleratorFont); 325 326 Rectangle iconRect = new Rectangle(); 327 Rectangle textRect = new Rectangle(); 328 Rectangle acceleratorRect = new Rectangle(); 329 Rectangle checkIconRect = new Rectangle(); 330 Rectangle arrowIconRect = new Rectangle(); 331 Rectangle viewRect = new Rectangle(Short.MAX_VALUE, Short.MAX_VALUE); 332 333 layoutMenuItem(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); 334 // find the union of the icon and text rects 335 Rectangle r = new Rectangle(); 336 r.setBounds(textRect); 337 r = SwingUtilities.computeUnion(iconRect.x, iconRect.y, iconRect.width, iconRect.height, r); 338 // r = iconRect.union(textRect); 339 340 // Add in the accelerator 341 boolean acceleratorTextIsEmpty = (keyString == null) || keyString.equals(""); 342 343 if (!acceleratorTextIsEmpty) { 344 r.width += acceleratorRect.width; 345 } 346 347 if (!isTopLevelMenu(b)) { 348 // Add in the checkIcon 349 r.width += checkIconRect.width; 350 r.width += defaultTextIconGap; 351 352 // Add in the arrowIcon space 353 r.width += defaultTextIconGap; 354 r.width += arrowIconRect.width; 355 } 356 357 final Insets insets = b.getInsets(); 358 if (insets != null) { 359 r.width += insets.left + insets.right; 360 r.height += insets.top + insets.bottom; 361 } 362 363 // Tweak for Mac 364 r.width += 4 + defaultTextIconGap; 365 r.height = Math.max(r.height, 18); 366 367 return r.getSize(); 368 } 369 370 protected void paintCheck(final Graphics g, final JMenuItem item, Icon checkIcon, Rectangle checkIconRect) { 371 if (isTopLevelMenu(item) || !item.isSelected()) return; 372 373 if (item.isArmed() && checkIcon instanceof InvertableIcon) { 374 ((InvertableIcon)checkIcon).getInvertedIcon().paintIcon(item, g, checkIconRect.x, checkIconRect.y); 375 } else { 376 checkIcon.paintIcon(item, g, checkIconRect.x, checkIconRect.y); 377 } 378 } 379 380 protected void paintIcon(final Graphics g, final JMenuItem c, final Rectangle localIconRect, boolean isEnabled) { 381 final ButtonModel model = c.getModel(); 382 Icon icon; 383 if (!isEnabled) { 384 icon = c.getDisabledIcon(); 385 } else if (model.isPressed() && model.isArmed()) { 386 icon = c.getPressedIcon(); 387 if (icon == null) { 388 // Use default icon 389 icon = c.getIcon(); 390 } 391 } else { 392 icon = c.getIcon(); 393 } 394 395 if (icon != null) icon.paintIcon(c, g, localIconRect.x, localIconRect.y); 396 } 397 398 protected void paintArrow(Graphics g, JMenuItem c, ButtonModel model, Icon arrowIcon, Rectangle arrowIconRect) { 399 if (isTopLevelMenu(c)) return; 400 401 if (c instanceof JMenu && (model.isArmed() || model.isSelected()) && arrowIcon instanceof InvertableIcon) { 402 ((InvertableIcon)arrowIcon).getInvertedIcon().paintIcon(c, g, arrowIconRect.x, arrowIconRect.y); 403 } else { 404 arrowIcon.paintIcon(c, g, arrowIconRect.x, arrowIconRect.y); 405 } 406 } 407 408 /** Draw a string with the graphics g at location (x,y) just like g.drawString() would. 409 * The first occurrence of underlineChar in text will be underlined. The matching is 410 * not case sensitive. 411 */ 412 public 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) { 413 char lc, uc; 414 int index = -1, lci, uci; 415 416 if (underlinedChar != '\0') { 417 uc = Character.toUpperCase((char)underlinedChar); 418 lc = Character.toLowerCase((char)underlinedChar); 419 420 uci = text.indexOf(uc); 421 lci = text.indexOf(lc); 422 423 if (uci == -1) index = lci; 424 else if (lci == -1) index = uci; 425 else index = (lci < uci) ? lci : uci; 426 } 427 428 SwingUtilities2.drawStringUnderlineCharAt(c, g, text, index, x, y); 429 } 430 431 /* 432 * Returns false if the component is a JMenu and it is a top 433 * level menu (on the menubar). 434 */ 435 private static boolean isTopLevelMenu(final JMenuItem menuItem) { 436 return (menuItem instanceof JMenu) && (((JMenu)menuItem).isTopLevelMenu()); 437 } 438 439 private 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) { 440 // Force it to do "LEFT", then flip the rects if we're right-to-left 441 SwingUtilities.layoutCompoundLabel(menuItem, fm, text, icon, verticalAlignment, SwingConstants.LEFT, verticalTextPosition, horizontalTextPosition, viewR, iconR, textR, textIconGap); 442 443 final boolean acceleratorTextIsEmpty = (keyString == null) || keyString.equals(""); 444 445 if (acceleratorTextIsEmpty) { 446 acceleratorR.width = acceleratorR.height = 0; 447 keyString = ""; 448 } else { 449 // Accel space doesn't overlap arrow space, even though items can't have both 450 acceleratorR.width = SwingUtilities.computeStringWidth(fmAccel, modifiersString); 451 // The keyStrings should all line up, so always adjust the width by the same amount 452 // (if they're multi-char, they won't line up but at least they won't be cut off) 453 acceleratorR.width += Math.max(fm.charWidth('M'), SwingUtilities.computeStringWidth(fm, keyString)); 454 acceleratorR.height = fmAccel.getHeight(); 455 } 456 457 /* Initialize the checkIcon bounds rectangle checkIconR. 458 */ 459 460 final boolean isTopLevelMenu = isTopLevelMenu(menuItem); 461 if (!isTopLevelMenu) { 462 if (checkIcon != null) { 463 checkIconR.width = checkIcon.getIconWidth(); 464 checkIconR.height = checkIcon.getIconHeight(); 465 } else { 466 checkIconR.width = checkIconR.height = 16; 467 } 468 469 /* Initialize the arrowIcon bounds rectangle arrowIconR. 470 */ 471 472 if (arrowIcon != null) { 473 arrowIconR.width = arrowIcon.getIconWidth(); 474 arrowIconR.height = arrowIcon.getIconHeight(); 475 } else { 476 arrowIconR.width = arrowIconR.height = 16; 477 } 478 479 textR.x += 12; 480 iconR.x += 12; 481 } 482 483 final Rectangle labelR = iconR.union(textR); 484 485 // Position the Accelerator text rect 486 // Menu shortcut text *ought* to have the letters left-justified - look at a menu with an "M" in it 487 acceleratorR.x += (viewR.width - arrowIconR.width - acceleratorR.width); 488 acceleratorR.y = viewR.y + (viewR.height / 2) - (acceleratorR.height / 2); 489 490 if (!isTopLevelMenu) { 491 // if ( GetSysDirection() < 0 ) hierRect.right = hierRect.left + w + 4; 492 // else hierRect.left = hierRect.right - w - 4; 493 arrowIconR.x = (viewR.width - arrowIconR.width) + 1; 494 arrowIconR.y = viewR.y + (labelR.height / 2) - (arrowIconR.height / 2) + 1; 495 496 checkIconR.y = viewR.y + (labelR.height / 2) - (checkIconR.height / 2); 497 checkIconR.x = 5; 498 499 textR.width += 8; 500 } 501 502 /*System.out.println("Layout: " +horizontalAlignment+ " v=" +viewR+" c="+checkIconR+" i="+ 503 iconR+" t="+textR+" acc="+acceleratorR+" a="+arrowIconR);*/ 504 505 if (!AquaUtils.isLeftToRight(menuItem)) { 506 // Flip the rectangles so that instead of [check][icon][text][accel/arrow] it's [accel/arrow][text][icon][check] 507 final int w = viewR.width; 508 checkIconR.x = w - (checkIconR.x + checkIconR.width); 509 iconR.x = w - (iconR.x + iconR.width); 510 textR.x = w - (textR.x + textR.width); 511 acceleratorR.x = w - (acceleratorR.x + acceleratorR.width); 512 arrowIconR.x = w - (arrowIconR.x + arrowIconR.width); 513 } 514 textR.x += menuItemGap; 515 iconR.x += menuItemGap; 516 517 return text; 518 } 519 520 public static Border getMenuBarPainter() { 521 final AquaBorder border = new AquaBorder.Default(); 522 border.painter.state.set(Widget.MENU_BAR); 523 return border; 524 } 525 526 public static Border getSelectedMenuBarItemPainter() { 527 final AquaBorder border = new AquaBorder.Default(); 528 border.painter.state.set(Widget.MENU_TITLE); 529 border.painter.state.set(State.PRESSED); 530 return border; 531 } 532 533 public static Border getSelectedMenuItemPainter() { 534 final AquaBorder border = new AquaBorder.Default(); 535 border.painter.state.set(Widget.MENU_ITEM); 536 border.painter.state.set(State.PRESSED); 537 return border; 538 } 539 }