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