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