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 }