1 /*
   2  * Copyright (c) 2010, 2017, 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 javafx.scene.control.skin;
  27 
  28 import static com.sun.javafx.FXPermissions.ACCESS_WINDOW_LIST_PERMISSION;
  29 
  30 import com.sun.javafx.scene.traversal.Direction;
  31 import javafx.css.converter.EnumConverter;
  32 import javafx.css.converter.SizeConverter;
  33 import com.sun.javafx.scene.control.MenuBarButton;
  34 import com.sun.javafx.scene.control.skin.Utils;
  35 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
  36 import javafx.beans.InvalidationListener;
  37 import javafx.beans.property.DoubleProperty;
  38 import javafx.beans.property.ObjectProperty;
  39 import javafx.beans.property.ReadOnlyProperty;
  40 import javafx.beans.value.ChangeListener;
  41 import javafx.beans.value.WeakChangeListener;
  42 import javafx.beans.value.WritableValue;
  43 import javafx.collections.ListChangeListener;
  44 import javafx.collections.MapChangeListener;
  45 import javafx.collections.ObservableList;
  46 import javafx.css.CssMetaData;
  47 import javafx.css.Styleable;
  48 import javafx.css.StyleableDoubleProperty;
  49 import javafx.css.StyleableObjectProperty;
  50 import javafx.css.StyleableProperty;
  51 import javafx.event.ActionEvent;
  52 import javafx.event.EventHandler;
  53 import javafx.event.WeakEventHandler;
  54 import javafx.geometry.Bounds;
  55 import javafx.geometry.NodeOrientation;
  56 import javafx.geometry.Pos;
  57 import javafx.scene.AccessibleAttribute;
  58 import javafx.scene.Node;
  59 import javafx.scene.Scene;
  60 import javafx.scene.control.Control;
  61 import javafx.scene.control.CustomMenuItem;
  62 import javafx.scene.control.Menu;
  63 import javafx.scene.control.MenuBar;
  64 import javafx.scene.control.MenuButton;
  65 import javafx.scene.control.MenuItem;
  66 import javafx.scene.control.SeparatorMenuItem;
  67 import javafx.scene.control.Skin;
  68 import javafx.scene.control.SkinBase;
  69 import javafx.scene.input.KeyCombination;
  70 import javafx.scene.input.KeyEvent;
  71 import javafx.scene.input.MouseEvent;
  72 import javafx.scene.layout.HBox;
  73 import javafx.stage.Stage;
  74 
  75 import static javafx.scene.input.KeyCode.*;
  76 
  77 import java.lang.ref.Reference;
  78 import java.lang.ref.WeakReference;
  79 import java.util.ArrayList;
  80 import java.util.Collections;
  81 import java.util.Iterator;
  82 import java.util.List;
  83 import java.util.Map;
  84 import java.util.Optional;
  85 import java.util.WeakHashMap;
  86 
  87 import com.sun.javafx.menu.MenuBase;
  88 import com.sun.javafx.scene.ParentHelper;
  89 import com.sun.javafx.scene.SceneHelper;
  90 import com.sun.javafx.scene.control.GlobalMenuAdapter;
  91 import com.sun.javafx.tk.Toolkit;
  92 import java.util.function.Predicate;
  93 import javafx.stage.Window;
  94 import javafx.util.Pair;
  95 
  96 import java.security.AccessController;
  97 import java.security.PrivilegedAction;
  98 
  99 /**
 100  * Default skin implementation for the {@link MenuBar} control. In essence it is
 101  * a simple toolbar. For the time being there is no overflow behavior and we just
 102  * hide nodes which fall outside the bounds.
 103  *
 104  * @see MenuBar
 105  * @since 9
 106  */
 107 public class MenuBarSkin extends SkinBase<MenuBar> {
 108 
 109     private static final ObservableList<Window> stages;
 110 
 111     static {
 112         final Predicate<Window> findStage = (w) -> w instanceof Stage;
 113         ObservableList<Window> windows = AccessController.doPrivileged(
 114             (PrivilegedAction<ObservableList<Window>>) () -> Window.getWindows(),
 115             null,
 116             ACCESS_WINDOW_LIST_PERMISSION);
 117         stages = windows.filtered(findStage);
 118     }
 119 
 120     /***************************************************************************
 121      *                                                                         *
 122      * Private fields                                                          *
 123      *                                                                         *
 124      **************************************************************************/
 125 
 126     private final HBox container;
 127 
 128     // represents the currently _open_ menu
 129     private Menu openMenu;
 130     private MenuBarButton openMenuButton;
 131 
 132     // represents the currently _focused_ menu. If openMenu is non-null, this should equal
 133     // openMenu. If openMenu is null, this can be any menu in the menu bar.
 134     private Menu focusedMenu;
 135     private int focusedMenuIndex = -1;
 136 
 137     private static WeakHashMap<Stage, Reference<MenuBarSkin>> systemMenuMap;
 138     private static List<MenuBase> wrappedDefaultMenus = new ArrayList<>();
 139     private static Stage currentMenuBarStage;
 140     private List<MenuBase> wrappedMenus;
 141 
 142     private WeakEventHandler<KeyEvent> weakSceneKeyEventHandler;
 143     private WeakEventHandler<MouseEvent> weakSceneMouseEventHandler;
 144     private WeakEventHandler<KeyEvent> weakSceneAltKeyEventHandler;
 145     private WeakChangeListener<Boolean> weakWindowFocusListener;
 146     private WeakChangeListener<Window> weakWindowSceneListener;
 147     private EventHandler<KeyEvent> keyEventHandler;
 148     private EventHandler<KeyEvent> altKeyEventHandler;
 149     private EventHandler<MouseEvent> mouseEventHandler;
 150     private ChangeListener<Boolean> menuBarFocusedPropertyListener;
 151     private ChangeListener<Scene> sceneChangeListener;
 152     private ChangeListener<Boolean> menuVisibilityChangeListener;
 153 
 154     private boolean pendingDismiss = false;
 155 
 156     private boolean altKeyPressed = false;
 157 
 158 
 159     /***************************************************************************
 160      *                                                                         *
 161      * Listeners / Callbacks                                                   *
 162      *                                                                         *
 163      **************************************************************************/
 164 
 165     // RT-20411 : reset menu selected/focused state
 166     private EventHandler<ActionEvent> menuActionEventHandler = t -> {
 167         if (t.getSource() instanceof CustomMenuItem) {
 168             // RT-29614 If CustomMenuItem hideOnClick is false, dont hide
 169             CustomMenuItem cmi = (CustomMenuItem)t.getSource();
 170             if (!cmi.isHideOnClick()) return;
 171         }
 172         unSelectMenus();
 173     };
 174 
 175     private ListChangeListener<MenuItem> menuItemListener = (c) -> {
 176         while (c.next()) {
 177             for (MenuItem mi : c.getAddedSubList()) {
 178                 updateActionListeners(mi, true);
 179             }
 180             for (MenuItem mi: c.getRemoved()) {
 181                 updateActionListeners(mi, false);
 182             }
 183         }
 184     };
 185 
 186     Runnable firstMenuRunnable = new Runnable() {
 187         public void run() {
 188             /*
 189             ** check that this menubar's container has contents,
 190             ** and that the first item is a MenuButton....
 191             ** otherwise the transfer is off!
 192             */
 193             if (container.getChildren().size() > 0) {
 194                 if (container.getChildren().get(0) instanceof MenuButton) {
 195 //                        container.getChildren().get(0).requestFocus();
 196                     if (focusedMenuIndex != 0) {
 197                         unSelectMenus();
 198                         menuModeStart(0);
 199                         openMenuButton = ((MenuBarButton)container.getChildren().get(0));
 200 //                        openMenu = getSkinnable().getMenus().get(0);
 201                         openMenuButton.setHover();
 202                     }
 203                     else {
 204                         unSelectMenus();
 205                     }
 206                 }
 207             }
 208         }
 209     };
 210 
 211 
 212 
 213     /***************************************************************************
 214      *                                                                         *
 215      * Constructors                                                            *
 216      *                                                                         *
 217      **************************************************************************/
 218 
 219     /**
 220      * Creates a new MenuBarSkin instance, installing the necessary child
 221      * nodes into the Control {@link Control#getChildren() children} list, as
 222      * well as the necessary input mappings for handling key, mouse, etc events.
 223      *
 224      * @param control The control that this skin should be installed onto.
 225      */
 226     public MenuBarSkin(final MenuBar control) {
 227         super(control);
 228 
 229         container = new HBox();
 230         container.getStyleClass().add("container");
 231         getChildren().add(container);
 232 
 233         // Key navigation
 234         keyEventHandler = event -> {
 235             // process right left and may be tab key events
 236             if (focusedMenu != null) {
 237                 switch (event.getCode()) {
 238                     case LEFT: {
 239                         boolean isRTL = control.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
 240                         if (control.getScene().getWindow().isFocused()) {
 241                             if (openMenu != null && !openMenu.isShowing()) {
 242                                 if (isRTL) {
 243                                     moveToMenu(Direction.NEXT, false); // just move the selection bar
 244                                 } else {
 245                                     moveToMenu(Direction.PREVIOUS, false); // just move the selection bar
 246                                 }
 247                                 event.consume();
 248                                 return;
 249                             }
 250                             if (isRTL) {
 251                                 moveToMenu(Direction.NEXT, true);
 252                             } else {
 253                                 moveToMenu(Direction.PREVIOUS, true);
 254                             }
 255                         }
 256                         event.consume();
 257                         break;
 258                     }
 259                     case RIGHT:
 260                     {
 261                         boolean isRTL = control.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
 262                         if (control.getScene().getWindow().isFocused()) {
 263                             if (openMenu != null && !openMenu.isShowing()) {
 264                                 if (isRTL) {
 265                                     moveToMenu(Direction.PREVIOUS, false); // just move the selection bar
 266                                 } else {
 267                                     moveToMenu(Direction.NEXT, false); // just move the selection bar
 268                                 }
 269                                 event.consume();
 270                                 return;
 271                             }
 272                             if (isRTL) {
 273                                 moveToMenu(Direction.PREVIOUS, true);
 274                             } else {
 275                                 moveToMenu(Direction.NEXT, true);
 276                             }
 277                         }
 278                         event.consume();
 279                         break;
 280                     }
 281                     case DOWN:
 282                     //case SPACE:
 283                     //case ENTER:
 284                         // RT-18859: Doing nothing for space and enter
 285                         if (control.getScene().getWindow().isFocused()) {
 286                             if (focusedMenuIndex != -1) {
 287                                 Menu menuToOpen = getSkinnable().getMenus().get(focusedMenuIndex);
 288                                 showMenu(menuToOpen, true);
 289                                 event.consume();
 290                             }
 291                         }
 292                         break;
 293                     case ESCAPE:
 294                         unSelectMenus();
 295                         event.consume();
 296                         break;
 297                 default:
 298                     break;
 299                 }
 300             }
 301         };
 302         menuBarFocusedPropertyListener = (ov, t, t1) -> {
 303             if (t1) {
 304                 // RT-23147 when MenuBar's focusTraversable is true the first
 305                 // menu will visually indicate focus
 306                 unSelectMenus();
 307                 menuModeStart(0);
 308                 openMenuButton = ((MenuBarButton)container.getChildren().get(0));
 309                 setFocusedMenuIndex(0);
 310                 openMenuButton.setHover();
 311             } else {
 312                 unSelectMenus();
 313              }
 314          };
 315         weakSceneKeyEventHandler = new WeakEventHandler<KeyEvent>(keyEventHandler);
 316         Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> {
 317             scene.addEventFilter(KeyEvent.KEY_PRESSED, weakSceneKeyEventHandler);
 318         });
 319 
 320         // When we click else where in the scene - menu selection should be cleared.
 321         mouseEventHandler = t -> {
 322             Bounds containerScreenBounds = container.localToScreen(container.getLayoutBounds());
 323             if (containerScreenBounds == null || !containerScreenBounds.contains(t.getScreenX(), t.getScreenY())) {
 324                 unSelectMenus();
 325             }
 326         };
 327         weakSceneMouseEventHandler = new WeakEventHandler<MouseEvent>(mouseEventHandler);
 328         Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> {
 329             scene.addEventFilter(MouseEvent.MOUSE_CLICKED, weakSceneMouseEventHandler);
 330         });
 331 
 332         weakWindowFocusListener = new WeakChangeListener<Boolean>((ov, t, t1) -> {
 333             if (!t1) {
 334               unSelectMenus();
 335             }
 336         });
 337         // When the parent window looses focus - menu selection should be cleared
 338         Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> {
 339             if (scene.getWindow() != null) {
 340                 scene.getWindow().focusedProperty().addListener(weakWindowFocusListener);
 341             } else {
 342                 ChangeListener<Window> sceneWindowListener = (observable, oldValue, newValue) -> {
 343                     if (oldValue != null)
 344                         oldValue.focusedProperty().removeListener(weakWindowFocusListener);
 345                     if (newValue != null)
 346                         newValue.focusedProperty().addListener(weakWindowFocusListener);
 347                 };
 348                 weakWindowSceneListener = new WeakChangeListener<>(sceneWindowListener);
 349                 scene.windowProperty().addListener(weakWindowSceneListener);
 350             }
 351         });
 352 
 353         menuVisibilityChangeListener = (ov, t, t1) -> {
 354             rebuildUI();
 355         };
 356 
 357         rebuildUI();
 358         control.getMenus().addListener((ListChangeListener<Menu>) c -> {
 359             rebuildUI();
 360         });
 361 
 362         if (Toolkit.getToolkit().getSystemMenu().isSupported()) {
 363             control.useSystemMenuBarProperty().addListener(valueModel -> {
 364                 rebuildUI();
 365             });
 366         }
 367 
 368         // When the mouse leaves the menu, the last hovered item should lose
 369         // it's focus so that it is no longer selected. This code returns focus
 370         // to the MenuBar itself, such that keyboard navigation can continue.
 371           // fix RT-12254 : menu bar should not request focus on mouse exit.
 372 //        addEventFilter(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() {
 373 //            @Override
 374 //            public void handle(MouseEvent event) {
 375 //                requestFocus();
 376 //            }
 377 //        });
 378 
 379         /*
 380         ** add an accelerator for F10 on windows and ctrl+F10 on mac/linux
 381         ** pressing f10 will select the first menu button on a menubar
 382         */
 383         final KeyCombination acceleratorKeyCombo;
 384         if (com.sun.javafx.util.Utils.isMac()) {
 385            acceleratorKeyCombo = KeyCombination.keyCombination("ctrl+F10");
 386         } else {
 387            acceleratorKeyCombo = KeyCombination.keyCombination("F10");
 388         }
 389 
 390         altKeyEventHandler = e -> {
 391             if (e.getEventType() == KeyEvent.KEY_PRESSED) {
 392                 // Clear menu selection when ALT is pressed by itself
 393                 altKeyPressed = false;
 394                 if (e.getCode() == ALT && !e.isConsumed()) {
 395                     if (focusedMenuIndex == -1) {
 396                         altKeyPressed = true;
 397                     }
 398                     unSelectMenus();
 399                 }
 400             } else if (e.getEventType() == KeyEvent.KEY_RELEASED) {
 401                 // Put focus on the first menu when ALT is released
 402                 // directly after being pressed by itself
 403                 if (altKeyPressed && e.getCode() == ALT && !e.isConsumed()) {
 404                     firstMenuRunnable.run();
 405                 }
 406                 altKeyPressed = false;
 407             }
 408         };
 409         weakSceneAltKeyEventHandler = new WeakEventHandler<>(altKeyEventHandler);
 410 
 411         Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> {
 412             scene.getAccelerators().put(acceleratorKeyCombo, firstMenuRunnable);
 413             scene.addEventHandler(KeyEvent.ANY, weakSceneAltKeyEventHandler);
 414         });
 415 
 416         ParentTraversalEngine engine = new ParentTraversalEngine(getSkinnable());
 417         engine.addTraverseListener((node, bounds) -> {
 418             if (openMenu != null) openMenu.hide();
 419             setFocusedMenuIndex(0);
 420         });
 421         ParentHelper.setTraversalEngine(getSkinnable(), engine);
 422 
 423         control.sceneProperty().addListener((ov, t, t1) -> {
 424             // remove event handlers / filters from the old scene (t)
 425             if (t != null) {
 426                 if (weakSceneKeyEventHandler != null) {
 427                     t.removeEventFilter(KeyEvent.KEY_PRESSED, weakSceneKeyEventHandler);
 428                 }
 429                 if (weakSceneMouseEventHandler != null) {
 430                     t.removeEventFilter(MouseEvent.MOUSE_CLICKED, weakSceneMouseEventHandler);
 431                 }
 432                 if (weakSceneAltKeyEventHandler != null) {
 433                     t.removeEventHandler(KeyEvent.ANY, weakSceneAltKeyEventHandler);
 434                 }
 435             }
 436 
 437             /**
 438              * remove the f10 accelerator from the old scene
 439              * add it to the new scene
 440              */
 441             if (t != null) {
 442                 t.getAccelerators().remove(acceleratorKeyCombo);
 443             }
 444             if (t1 != null ) {
 445                 t1.getAccelerators().put(acceleratorKeyCombo, firstMenuRunnable);
 446             }
 447         });
 448     }
 449 
 450     private void showMenu(Menu menu) {
 451         showMenu(menu, false);
 452     }
 453 
 454     private void showMenu(Menu menu, boolean selectFirstItem) {
 455         // hide the currently visible menu, and move to the next one
 456         if (openMenu == menu) return;
 457         if (openMenu != null) {
 458             openMenu.hide();
 459         }
 460 
 461         openMenu = menu;
 462         if (!menu.isShowing() && !isMenuEmpty(menu)) {
 463             if (selectFirstItem) {
 464                 // put selection / focus on first item in menu
 465                 MenuButton menuButton = getNodeForMenu(focusedMenuIndex);
 466                 Skin<?> skin = menuButton.getSkin();
 467                 if (skin instanceof MenuButtonSkinBase) {
 468                     ((MenuButtonSkinBase)skin).requestFocusOnFirstMenuItem();
 469                 }
 470             }
 471 
 472             openMenu.show();
 473         }
 474     }
 475 
 476     private void setFocusedMenuIndex(int index) {
 477         this.focusedMenuIndex = index;
 478         focusedMenu = index == -1 ? null : getSkinnable().getMenus().get(index);
 479 
 480         if (focusedMenu != null && focusedMenuIndex != -1) {
 481             openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex);
 482             openMenuButton.setHover();
 483         }
 484     }
 485 
 486 
 487 
 488     /***************************************************************************
 489      *                                                                         *
 490      * Static methods                                                          *
 491      *                                                                         *
 492      **************************************************************************/
 493 
 494     // RT-22480: This is intended as private API for SceneBuilder,
 495     // pending fix for RT-19857: Keeping menu in the Mac menu bar when
 496     // there is no more stage
 497     /**
 498      * Set the default system menu bar. This allows an application to keep menu
 499      * in the system menu bar after the last Window is closed.
 500      * @param menuBar the menu bar
 501      */
 502     public static void setDefaultSystemMenuBar(final MenuBar menuBar) {
 503         if (Toolkit.getToolkit().getSystemMenu().isSupported()) {
 504             wrappedDefaultMenus.clear();
 505             for (Menu menu : menuBar.getMenus()) {
 506                 wrappedDefaultMenus.add(GlobalMenuAdapter.adapt(menu));
 507             }
 508             menuBar.getMenus().addListener((ListChangeListener<Menu>) c -> {
 509                 wrappedDefaultMenus.clear();
 510                 for (Menu menu : menuBar.getMenus()) {
 511                     wrappedDefaultMenus.add(GlobalMenuAdapter.adapt(menu));
 512                 }
 513             });
 514         }
 515     }
 516 
 517     private static MenuBarSkin getMenuBarSkin(Stage stage) {
 518         if (systemMenuMap == null) return null;
 519         Reference<MenuBarSkin> skinRef = systemMenuMap.get(stage);
 520         return skinRef == null ? null : skinRef.get();
 521     }
 522 
 523     private static void setSystemMenu(Stage stage) {
 524         if (stage != null && stage.isFocused()) {
 525             while (stage != null && stage.getOwner() instanceof Stage) {
 526                 MenuBarSkin skin = getMenuBarSkin(stage);
 527                 if (skin != null && skin.wrappedMenus != null) {
 528                     break;
 529                 } else {
 530                     // This is a secondary stage (dialog) that doesn't
 531                     // have own menu bar.
 532                     //
 533                     // Continue looking for a menu bar in the parent stage.
 534                     stage = (Stage)stage.getOwner();
 535                 }
 536             }
 537         } else {
 538             stage = null;
 539         }
 540 
 541         if (stage != currentMenuBarStage) {
 542             List<MenuBase> menuList = null;
 543             if (stage != null) {
 544                 MenuBarSkin skin = getMenuBarSkin(stage);
 545                 if (skin != null) {
 546                     menuList = skin.wrappedMenus;
 547                 }
 548             }
 549             if (menuList == null) {
 550                 menuList = wrappedDefaultMenus;
 551             }
 552             Toolkit.getToolkit().getSystemMenu().setMenus(menuList);
 553             currentMenuBarStage = stage;
 554         }
 555     }
 556 
 557     private static void initSystemMenuBar() {
 558         systemMenuMap = new WeakHashMap<>();
 559 
 560         final InvalidationListener focusedStageListener = ov -> {
 561             setSystemMenu((Stage)((ReadOnlyProperty<?>)ov).getBean());
 562         };
 563 
 564         for (Window stage : stages) {
 565             stage.focusedProperty().addListener(focusedStageListener);
 566         }
 567         stages.addListener((ListChangeListener<Window>) c -> {
 568             while (c.next()) {
 569                 for (Window stage : c.getRemoved()) {
 570                     stage.focusedProperty().removeListener(focusedStageListener);
 571                 }
 572                 for (Window stage : c.getAddedSubList()) {
 573                     stage.focusedProperty().addListener(focusedStageListener);
 574                     setSystemMenu((Stage) stage);
 575                 }
 576             }
 577         });
 578     }
 579 
 580 
 581 
 582     /***************************************************************************
 583      *                                                                         *
 584      * Properties                                                              *
 585      *                                                                         *
 586      **************************************************************************/
 587 
 588     /**
 589      * Specifies the spacing between menu buttons on the MenuBar.
 590      */
 591     // --- spacing
 592     private DoubleProperty spacing;
 593     public final void setSpacing(double value) {
 594         spacingProperty().set(snapSpaceX(value));
 595     }
 596 
 597     public final double getSpacing() {
 598         return spacing == null ? 0.0 : snapSpaceX(spacing.get());
 599     }
 600 
 601     public final DoubleProperty spacingProperty() {
 602         if (spacing == null) {
 603             spacing = new StyleableDoubleProperty() {
 604 
 605                 @Override
 606                 protected void invalidated() {
 607                     final double value = get();
 608                     container.setSpacing(value);
 609                 }
 610 
 611                 @Override
 612                 public Object getBean() {
 613                     return MenuBarSkin.this;
 614                 }
 615 
 616                 @Override
 617                 public String getName() {
 618                     return "spacing";
 619                 }
 620 
 621                 @Override
 622                 public CssMetaData<MenuBar,Number> getCssMetaData() {
 623                     return SPACING;
 624                 }
 625             };
 626         }
 627         return spacing;
 628     }
 629 
 630     /**
 631      * Specifies the alignment of the menu buttons inside the MenuBar (by default
 632      * it is Pos.TOP_LEFT).
 633      */
 634     // --- container alignment
 635     private ObjectProperty<Pos> containerAlignment;
 636     public final void setContainerAlignment(Pos value) {
 637         containerAlignmentProperty().set(value);
 638     }
 639 
 640     public final Pos getContainerAlignment() {
 641         return containerAlignment == null ? Pos.TOP_LEFT : containerAlignment.get();
 642     }
 643 
 644     public final ObjectProperty<Pos> containerAlignmentProperty() {
 645         if (containerAlignment == null) {
 646             containerAlignment = new StyleableObjectProperty<Pos>(Pos.TOP_LEFT) {
 647 
 648                 @Override
 649                 public void invalidated() {
 650                     final Pos value = get();
 651                     container.setAlignment(value);
 652                 }
 653 
 654                 @Override
 655                 public Object getBean() {
 656                     return MenuBarSkin.this;
 657                 }
 658 
 659                 @Override
 660                 public String getName() {
 661                     return "containerAlignment";
 662                 }
 663 
 664                 @Override
 665                 public CssMetaData<MenuBar,Pos> getCssMetaData() {
 666                     return ALIGNMENT;
 667                 }
 668             };
 669         }
 670         return containerAlignment;
 671     }
 672 
 673 
 674 
 675     /***************************************************************************
 676      *                                                                         *
 677      * Public API                                                              *
 678      *                                                                         *
 679      **************************************************************************/
 680 
 681     /** {@inheritDoc} */
 682     @Override public void dispose() {
 683         cleanUpSystemMenu();
 684         // call super.dispose last since it sets control to null
 685         super.dispose();
 686     }
 687 
 688     // Return empty insets when "container" is empty, which happens
 689     // when using the system menu bar.
 690 
 691     /** {@inheritDoc} */
 692     @Override protected double snappedTopInset() {
 693         return container.getChildren().isEmpty() ? 0 : super.snappedTopInset();
 694     }
 695     /** {@inheritDoc} */
 696     @Override protected double snappedBottomInset() {
 697         return container.getChildren().isEmpty() ? 0 : super.snappedBottomInset();
 698     }
 699     /** {@inheritDoc} */
 700     @Override protected double snappedLeftInset() {
 701         return container.getChildren().isEmpty() ? 0 : super.snappedLeftInset();
 702     }
 703     /** {@inheritDoc} */
 704     @Override protected double snappedRightInset() {
 705         return container.getChildren().isEmpty() ? 0 : super.snappedRightInset();
 706     }
 707 
 708     /**
 709      * Layout the menu bar. This is a simple horizontal layout like an hbox.
 710      * Any menu items which don't fit into it will simply be made invisible.
 711      */
 712     /** {@inheritDoc} */
 713     @Override protected void layoutChildren(final double x, final double y,
 714                                             final double w, final double h) {
 715         // layout the menus one after another
 716         container.resizeRelocate(x, y, w, h);
 717     }
 718 
 719     /** {@inheritDoc} */
 720     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 721         return container.minWidth(height) + snappedLeftInset() + snappedRightInset();
 722     }
 723 
 724     /** {@inheritDoc} */
 725     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 726         return container.prefWidth(height) + snappedLeftInset() + snappedRightInset();
 727     }
 728 
 729     /** {@inheritDoc} */
 730     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 731         return container.minHeight(width) + snappedTopInset() + snappedBottomInset();
 732     }
 733 
 734     /** {@inheritDoc} */
 735     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 736         return container.prefHeight(width) + snappedTopInset() + snappedBottomInset();
 737     }
 738 
 739     // grow horizontally, but not vertically
 740     /** {@inheritDoc} */
 741     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 742         return getSkinnable().prefHeight(-1);
 743     }
 744 
 745 
 746 
 747     /***************************************************************************
 748      *                                                                         *
 749      * Private implementation                                                  *
 750      *                                                                         *
 751      **************************************************************************/
 752 
 753     // For testing purpose only.
 754     MenuButton getNodeForMenu(int i) {
 755         if (i < container.getChildren().size()) {
 756             return (MenuBarButton)container.getChildren().get(i);
 757         }
 758         return null;
 759     }
 760 
 761     int getFocusedMenuIndex() {
 762         return focusedMenuIndex;
 763     }
 764 
 765     private boolean menusContainCustomMenuItem() {
 766         for (Menu menu : getSkinnable().getMenus()) {
 767             if (menuContainsCustomMenuItem(menu)) {
 768                 System.err.println("Warning: MenuBar ignored property useSystemMenuBar because menus contain CustomMenuItem");
 769                 return true;
 770             }
 771         }
 772         return false;
 773     }
 774 
 775     private boolean menuContainsCustomMenuItem(Menu menu) {
 776         for (MenuItem mi : menu.getItems()) {
 777             if (mi instanceof CustomMenuItem && !(mi instanceof SeparatorMenuItem)) {
 778                 return true;
 779             } else if (mi instanceof Menu) {
 780                 if (menuContainsCustomMenuItem((Menu)mi)) {
 781                     return true;
 782                 }
 783             }
 784         }
 785         return false;
 786     }
 787 
 788     private int getMenuBarButtonIndex(MenuBarButton m) {
 789         for (int i= 0; i < container.getChildren().size(); i++) {
 790             MenuBarButton menuButton = (MenuBarButton)container.getChildren().get(i);
 791             if (m == menuButton) {
 792                 return i;
 793             }
 794         }
 795         return -1;
 796     }
 797 
 798     private void updateActionListeners(MenuItem item, boolean add) {
 799         if (item instanceof Menu) {
 800             Menu menu = (Menu) item;
 801 
 802             if (add) {
 803                 menu.getItems().addListener(menuItemListener);
 804             } else {
 805                 menu.getItems().removeListener(menuItemListener);
 806             }
 807 
 808             for (MenuItem mi : menu.getItems()) {
 809                 updateActionListeners(mi, add);
 810             }
 811         } else {
 812             if (add) {
 813                 item.addEventHandler(ActionEvent.ACTION, menuActionEventHandler);
 814             } else {
 815                 item.removeEventHandler(ActionEvent.ACTION, menuActionEventHandler);
 816             }
 817         }
 818     }
 819 
 820     private void rebuildUI() {
 821         getSkinnable().focusedProperty().removeListener(menuBarFocusedPropertyListener);
 822         for (Menu m : getSkinnable().getMenus()) {
 823             // remove action listeners
 824             updateActionListeners(m, false);
 825 
 826             m.visibleProperty().removeListener(menuVisibilityChangeListener);
 827         }
 828         for (Node n : container.getChildren()) {
 829             // Stop observing menu's showing & disable property for changes.
 830             // Need to unbind before clearing container's children.
 831             MenuBarButton menuButton = (MenuBarButton)n;
 832             menuButton.hide();
 833             menuButton.menu.showingProperty().removeListener(menuButton.menuListener);
 834             menuButton.disableProperty().unbind();
 835             menuButton.textProperty().unbind();
 836             menuButton.graphicProperty().unbind();
 837             menuButton.styleProperty().unbind();
 838 
 839             menuButton.dispose();
 840 
 841             // RT-29729 : old instance of context menu window/popup for this MenuButton needs
 842             // to be cleaned up. Setting the skin to null - results in a call to dispose()
 843             // on the skin which in this case MenuButtonSkinBase - does the subsequent
 844             // clean up to ContextMenu/popup window.
 845             menuButton.setSkin(null);
 846             menuButton = null;
 847         }
 848         container.getChildren().clear();
 849 
 850         if (Toolkit.getToolkit().getSystemMenu().isSupported()) {
 851             final Scene scene = getSkinnable().getScene();
 852             if (scene != null) {
 853                 // RT-36554 - make sure system menu is updated when this MenuBar's scene changes.
 854                 if (sceneChangeListener == null) {
 855                     sceneChangeListener = (observable, oldValue, newValue) -> {
 856 
 857                         if (oldValue != null) {
 858                             if (oldValue.getWindow() instanceof Stage) {
 859                                 final Stage stage = (Stage) oldValue.getWindow();
 860                                 final MenuBarSkin curMBSkin = getMenuBarSkin(stage);
 861                                 if (curMBSkin == MenuBarSkin.this) {
 862                                     curMBSkin.wrappedMenus = null;
 863                                     systemMenuMap.remove(stage);
 864                                     if (currentMenuBarStage == stage) {
 865                                         currentMenuBarStage = null;
 866                                         setSystemMenu(stage);
 867                                     }
 868                                 } else {
 869                                     if (getSkinnable().isUseSystemMenuBar() &&
 870                                             curMBSkin != null && curMBSkin.getSkinnable() != null &&
 871                                             curMBSkin.getSkinnable().isUseSystemMenuBar()) {
 872                                         curMBSkin.getSkinnable().setUseSystemMenuBar(false);
 873                                     }
 874                                 }
 875                             }
 876                         }
 877 
 878                         if (newValue != null) {
 879                             if (getSkinnable().isUseSystemMenuBar() && !menusContainCustomMenuItem()) {
 880                                 if (newValue.getWindow() instanceof Stage) {
 881                                     final Stage stage = (Stage) newValue.getWindow();
 882                                     if (systemMenuMap == null) {
 883                                         initSystemMenuBar();
 884                                     }
 885                                     wrappedMenus = new ArrayList<>();
 886                                     systemMenuMap.put(stage, new WeakReference<>(this));
 887                                     for (Menu menu : getSkinnable().getMenus()) {
 888                                         wrappedMenus.add(GlobalMenuAdapter.adapt(menu));
 889                                     }
 890                                     currentMenuBarStage = null;
 891                                     setSystemMenu(stage);
 892 
 893                                     // TODO: Why two request layout calls here?
 894                                     getSkinnable().requestLayout();
 895                                     javafx.application.Platform.runLater(() -> getSkinnable().requestLayout());
 896                                 }
 897                             }
 898                         }
 899                     };
 900                     getSkinnable().sceneProperty().addListener(sceneChangeListener);
 901                 }
 902 
 903                 // Fake a change event to trigger an update to the system menu.
 904                 sceneChangeListener.changed(getSkinnable().sceneProperty(), scene, scene);
 905 
 906                 // If the system menu references this MenuBarSkin, then we're done with rebuilding the UI.
 907                 // If the system menu does not reference this MenuBarSkin, then the MenuBar is a child of the scene
 908                 // and we continue with the update.
 909                 // If there is no system menu but this skinnable uses the system menu bar, then the
 910                 // stage just isn't focused yet (see setSystemMenu) and we're done rebuilding the UI.
 911                 if (currentMenuBarStage != null ? getMenuBarSkin(currentMenuBarStage) == MenuBarSkin.this : getSkinnable().isUseSystemMenuBar()) {
 912                     return;
 913                 }
 914 
 915             } else {
 916                 // if scene is null, make sure this MenuBarSkin isn't left behind as the system menu
 917                 if (currentMenuBarStage != null) {
 918                     final MenuBarSkin curMBSkin = getMenuBarSkin(currentMenuBarStage);
 919                     if (curMBSkin == MenuBarSkin.this) {
 920                         setSystemMenu(null);
 921                     }
 922                 }
 923             }
 924         }
 925 
 926         getSkinnable().focusedProperty().addListener(menuBarFocusedPropertyListener);
 927         for (final Menu menu : getSkinnable().getMenus()) {
 928 
 929             menu.visibleProperty().addListener(menuVisibilityChangeListener);
 930 
 931             if (!menu.isVisible()) continue;
 932             final MenuBarButton menuButton = new MenuBarButton(this, menu);
 933             menuButton.setFocusTraversable(false);
 934             menuButton.getStyleClass().add("menu");
 935             menuButton.setStyle(menu.getStyle()); // copy style
 936 
 937             menuButton.getItems().setAll(menu.getItems());
 938             container.getChildren().add(menuButton);
 939 
 940             menuButton.menuListener = (observable, oldValue, newValue) -> {
 941                 if (menu.isShowing()) {
 942                     menuButton.show();
 943                     menuModeStart(container.getChildren().indexOf(menuButton));
 944                 } else {
 945                     menuButton.hide();
 946                 }
 947             };
 948             menuButton.menu = menu;
 949             menu.showingProperty().addListener(menuButton.menuListener);
 950             menuButton.disableProperty().bindBidirectional(menu.disableProperty());
 951             menuButton.textProperty().bind(menu.textProperty());
 952             menuButton.graphicProperty().bind(menu.graphicProperty());
 953             menuButton.styleProperty().bind(menu.styleProperty());
 954             menuButton.getProperties().addListener((MapChangeListener<Object, Object>) c -> {
 955                  if (c.wasAdded() && MenuButtonSkin.AUTOHIDE.equals(c.getKey())) {
 956                     menuButton.getProperties().remove(MenuButtonSkin.AUTOHIDE);
 957                     menu.hide();
 958                 }
 959             });
 960             menuButton.showingProperty().addListener((observable, oldValue, isShowing) -> {
 961                 if (isShowing) {
 962                     if(openMenuButton == null && focusedMenuIndex != -1)
 963                         openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex);
 964 
 965                     if (openMenuButton != null && openMenuButton != menuButton) {
 966                         openMenuButton.clearHover();
 967                     }
 968                     openMenuButton = menuButton;
 969                     showMenu(menu);
 970                 } else {
 971                     // Fix for JDK-8167138 - we need to clear out the openMenu / openMenuButton
 972                     // when the menu is hidden (e.g. via autoHide), so that we can open it again
 973                     // the next time (if it is the first menu requested to show)
 974                     openMenu = null;
 975                     openMenuButton = null;
 976                 }
 977             });
 978 
 979             menuButton.setOnMousePressed(event -> {
 980                 pendingDismiss = menuButton.isShowing();
 981 
 982                 // check if the owner window has focus
 983                 if (menuButton.getScene().getWindow().isFocused()) {
 984                     showMenu(menu);
 985                     // update FocusedIndex
 986                     menuModeStart(getMenuBarButtonIndex(menuButton));
 987                 }
 988             });
 989 
 990             menuButton.setOnMouseReleased(event -> {
 991                 // check if the owner window has focus
 992                 if (menuButton.getScene().getWindow().isFocused()) {
 993                     if (pendingDismiss) {
 994                         resetOpenMenu();
 995                     }
 996                 }
 997                 pendingDismiss = false;
 998             });
 999 
1000             menuButton.setOnMouseEntered(event -> {
1001                 // check if the owner window has focus
1002                 if (menuButton.getScene() != null && menuButton.getScene().getWindow() != null &&
1003                         menuButton.getScene().getWindow().isFocused()) {
1004                     if (openMenuButton != null && openMenuButton != menuButton) {
1005                             openMenuButton.clearHover();
1006                             openMenuButton = null;
1007                             openMenuButton = menuButton;
1008                     }
1009                     updateFocusedIndex();
1010                     if (openMenu != null && openMenu != menu) {
1011                         showMenu(menu);
1012                     }
1013                 }
1014             });
1015             updateActionListeners(menu, true);
1016         }
1017         getSkinnable().requestLayout();
1018     }
1019 
1020     private void cleanUpSystemMenu() {
1021         if (sceneChangeListener != null && getSkinnable() != null) {
1022             getSkinnable().sceneProperty().removeListener(sceneChangeListener);
1023             // rebuildUI creates sceneChangeListener and adds sceneChangeListener to sceneProperty,
1024             // so sceneChangeListener needs to be reset to null in the off chance that this
1025             // skin instance is reused.
1026             sceneChangeListener = null;
1027         }
1028 
1029         if (currentMenuBarStage != null && getMenuBarSkin(currentMenuBarStage) == MenuBarSkin.this) {
1030             setSystemMenu(null);
1031         }
1032 
1033         if (systemMenuMap != null) {
1034             Iterator<Map.Entry<Stage,Reference<MenuBarSkin>>> iterator = systemMenuMap.entrySet().iterator();
1035             while (iterator.hasNext()) {
1036                 Map.Entry<Stage,Reference<MenuBarSkin>> entry = iterator.next();
1037                 Reference<MenuBarSkin> ref = entry.getValue();
1038                 MenuBarSkin skin = ref != null ? ref.get() : null;
1039                 if (skin == null || skin == MenuBarSkin.this) {
1040                     iterator.remove();
1041                 }
1042             }
1043         }
1044     }
1045 
1046     private boolean isMenuEmpty(Menu menu) {
1047         boolean retVal = true;
1048         if (menu != null) {
1049             for (MenuItem m : menu.getItems()) {
1050                 if (m != null && m.isVisible()) retVal = false;
1051             }
1052         }
1053         return retVal;
1054     }
1055 
1056     private void resetOpenMenu() {
1057         if (openMenu != null) {
1058             openMenu.hide();
1059             openMenu = null;
1060         }
1061     }
1062 
1063     private void unSelectMenus() {
1064         clearMenuButtonHover();
1065         if (focusedMenuIndex == -1) return;
1066         if (openMenu != null) {
1067             openMenu.hide();
1068             openMenu = null;
1069         }
1070         if (openMenuButton != null) {
1071             openMenuButton.clearHover();
1072             openMenuButton = null;
1073         }
1074         menuModeEnd();
1075     }
1076 
1077     private void menuModeStart(int newIndex) {
1078         if (focusedMenuIndex == -1) {
1079             SceneHelper.getSceneAccessor().setTransientFocusContainer(getSkinnable().getScene(), getSkinnable());
1080         }
1081         setFocusedMenuIndex(newIndex);
1082     }
1083 
1084     private void menuModeEnd() {
1085         if (focusedMenuIndex != -1) {
1086             SceneHelper.getSceneAccessor().setTransientFocusContainer(getSkinnable().getScene(), null);
1087 
1088             /* Return the a11y focus to a control in the scene. */
1089             getSkinnable().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_NODE);
1090         }
1091         setFocusedMenuIndex(-1);
1092     }
1093 
1094     private void moveToMenu(Direction dir, boolean doShow) {
1095         boolean showNextMenu = doShow && focusedMenu.isShowing();
1096         findSibling(dir, focusedMenuIndex).ifPresent(p -> {
1097             setFocusedMenuIndex(p.getValue());
1098             if (showNextMenu) {
1099                 // we explicitly do *not* allow selection - we are moving
1100                 // to a sibling menu, and therefore selection should be reset
1101                 showMenu(p.getKey(), false);
1102             }
1103         });
1104     }
1105 
1106     private Optional<Pair<Menu,Integer>> findSibling(Direction dir, int startIndex) {
1107         if (startIndex == -1) {
1108             return Optional.empty();
1109         }
1110 
1111         final int totalMenus = getSkinnable().getMenus().size();
1112         int i = 0;
1113         int nextIndex = 0;
1114 
1115         // Traverse all menus in menubar to find nextIndex
1116         while (i < totalMenus) {
1117             i++;
1118 
1119             nextIndex = (startIndex + (dir.isForward() ? 1 : -1)) % totalMenus;
1120 
1121             if (nextIndex == -1) {
1122                 // loop backwards to end
1123                 nextIndex = totalMenus - 1;
1124             }
1125 
1126             // if menu at nextIndex is disabled, skip it
1127             if (getSkinnable().getMenus().get(nextIndex).isDisable()) {
1128                 // Calculate new nextIndex by continuing loop
1129                 startIndex = nextIndex;
1130             } else {
1131                 // nextIndex is to be highlighted
1132                 break;
1133             }
1134         }
1135 
1136         clearMenuButtonHover();
1137         return Optional.of(new Pair<>(getSkinnable().getMenus().get(nextIndex), nextIndex));
1138     }
1139 
1140     private void updateFocusedIndex() {
1141         int index = 0;
1142         for(Node n : container.getChildren()) {
1143             if (n.isHover()) {
1144                 setFocusedMenuIndex(index);
1145                 return;
1146             }
1147             index++;
1148         }
1149         menuModeEnd();
1150     }
1151 
1152     private void clearMenuButtonHover() {
1153          for(Node n : container.getChildren()) {
1154             if (n.isHover()) {
1155                 ((MenuBarButton)n).clearHover();
1156                 ((MenuBarButton)n).disarm();
1157                 return;
1158             }
1159         }
1160     }
1161 
1162 
1163 
1164     /***************************************************************************
1165      *                                                                         *
1166      * CSS                                                                     *
1167      *                                                                         *
1168      **************************************************************************/
1169 
1170     private static final CssMetaData<MenuBar,Number> SPACING =
1171             new CssMetaData<MenuBar,Number>("-fx-spacing",
1172                     SizeConverter.getInstance(), 0.0) {
1173 
1174                 @Override
1175                 public boolean isSettable(MenuBar n) {
1176                     final MenuBarSkin skin = (MenuBarSkin) n.getSkin();
1177                     return skin.spacing == null || !skin.spacing.isBound();
1178                 }
1179 
1180                 @Override
1181                 public StyleableProperty<Number> getStyleableProperty(MenuBar n) {
1182                     final MenuBarSkin skin = (MenuBarSkin) n.getSkin();
1183                     return (StyleableProperty<Number>)(WritableValue<Number>)skin.spacingProperty();
1184                 }
1185             };
1186 
1187     private static final CssMetaData<MenuBar,Pos> ALIGNMENT =
1188             new CssMetaData<MenuBar,Pos>("-fx-alignment",
1189                     new EnumConverter<Pos>(Pos.class), Pos.TOP_LEFT ) {
1190 
1191                 @Override
1192                 public boolean isSettable(MenuBar n) {
1193                     final MenuBarSkin skin = (MenuBarSkin) n.getSkin();
1194                     return skin.containerAlignment == null || !skin.containerAlignment.isBound();
1195                 }
1196 
1197                 @Override
1198                 public StyleableProperty<Pos> getStyleableProperty(MenuBar n) {
1199                     final MenuBarSkin skin = (MenuBarSkin) n.getSkin();
1200                     return (StyleableProperty<Pos>)(WritableValue<Pos>)skin.containerAlignmentProperty();
1201                 }
1202             };
1203 
1204 
1205     private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1206     static {
1207 
1208         final List<CssMetaData<? extends Styleable, ?>> styleables =
1209                 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
1210 
1211         // StackPane also has -fx-alignment. Replace it with
1212         // MenuBarSkin's.
1213         // TODO: Really should be able to reference StackPane.StyleableProperties.ALIGNMENT
1214         final String alignmentProperty = ALIGNMENT.getProperty();
1215         for (int n=0, nMax=styleables.size(); n<nMax; n++) {
1216             final CssMetaData<?,?> prop = styleables.get(n);
1217             if (alignmentProperty.equals(prop.getProperty())) styleables.remove(prop);
1218         }
1219 
1220         styleables.add(SPACING);
1221         styleables.add(ALIGNMENT);
1222         STYLEABLES = Collections.unmodifiableList(styleables);
1223 
1224     }
1225 
1226     /**
1227      * Returns the CssMetaData associated with this class, which may include the
1228      * CssMetaData of its superclasses.
1229      * @return the CssMetaData associated with this class, which may include the
1230      * CssMetaData of its superclasses
1231      */
1232     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1233         return STYLEABLES;
1234     }
1235 
1236     /**
1237      * {@inheritDoc}
1238      */
1239     @Override
1240     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1241         return getClassCssMetaData();
1242     }
1243 
1244     /***************************************************************************
1245      *                                                                         *
1246      * Accessibility handling                                                  *
1247      *                                                                         *
1248      **************************************************************************/
1249 
1250     /** {@inheritDoc} */
1251     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1252         switch (attribute) {
1253             case FOCUS_NODE: return openMenuButton;
1254             default: return super.queryAccessibleAttribute(attribute, parameters);
1255         }
1256     }
1257 }