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