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