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