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