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 || 393 e.getCode() == ALT_GRAPH) && !e.isConsumed()) { 394 if (focusedMenuIndex == -1) { 395 altKeyPressed = true; 396 } 397 unSelectMenus(); 398 } 399 } else if (e.getEventType() == KeyEvent.KEY_RELEASED) { 400 // Put focus on the first menu when ALT is released 401 // directly after being pressed by itself 402 if (altKeyPressed && (e.getCode() == ALT || 403 e.getCode() == ALT_GRAPH) && !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 && openMenuButton != menuButton) { 962 openMenuButton.hide(); 963 } 964 openMenuButton = menuButton; 965 showMenu(menu); 966 } else { 967 // Fix for JDK-8167138 - we need to clear out the openMenu / openMenuButton 968 // when the menu is hidden (e.g. via autoHide), so that we can open it again 969 // the next time (if it is the first menu requested to show) 970 openMenu = null; 971 openMenuButton = null; 972 } 973 }); 974 975 menuButton.setOnMousePressed(event -> { 976 pendingDismiss = menuButton.isShowing(); 977 978 // check if the owner window has focus 979 if (menuButton.getScene().getWindow().isFocused()) { 980 showMenu(menu); 981 // update FocusedIndex 982 menuModeStart(getMenuBarButtonIndex(menuButton)); 983 } 984 }); 985 986 menuButton.setOnMouseReleased(event -> { 987 // check if the owner window has focus 988 if (menuButton.getScene().getWindow().isFocused()) { 989 if (pendingDismiss) { 990 resetOpenMenu(); 991 } 992 } 993 pendingDismiss = false; 994 }); 995 996 menuButton.setOnMouseEntered(event -> { 997 // check if the owner window has focus 998 if (menuButton.getScene() != null && menuButton.getScene().getWindow() != null && 999 menuButton.getScene().getWindow().isFocused()) { 1000 if (openMenuButton != null && openMenuButton != menuButton) { 1001 openMenuButton.clearHover(); 1002 openMenuButton = null; 1003 openMenuButton = menuButton; 1004 } 1005 updateFocusedIndex(); 1006 if (openMenu != null && openMenu != menu) { 1007 showMenu(menu); 1008 } 1009 } 1010 }); 1011 updateActionListeners(menu, true); 1012 } 1013 getSkinnable().requestLayout(); 1014 } 1015 1016 private void cleanUpSystemMenu() { 1017 if (sceneChangeListener != null && getSkinnable() != null) { 1018 getSkinnable().sceneProperty().removeListener(sceneChangeListener); 1019 // rebuildUI creates sceneChangeListener and adds sceneChangeListener to sceneProperty, 1020 // so sceneChangeListener needs to be reset to null in the off chance that this 1021 // skin instance is reused. 1022 sceneChangeListener = null; 1023 } 1024 1025 if (currentMenuBarStage != null && getMenuBarSkin(currentMenuBarStage) == MenuBarSkin.this) { 1026 setSystemMenu(null); 1027 } 1028 1029 if (systemMenuMap != null) { 1030 Iterator<Map.Entry<Stage,Reference<MenuBarSkin>>> iterator = systemMenuMap.entrySet().iterator(); 1031 while (iterator.hasNext()) { 1032 Map.Entry<Stage,Reference<MenuBarSkin>> entry = iterator.next(); 1033 Reference<MenuBarSkin> ref = entry.getValue(); 1034 MenuBarSkin skin = ref != null ? ref.get() : null; 1035 if (skin == null || skin == MenuBarSkin.this) { 1036 iterator.remove(); 1037 } 1038 } 1039 } 1040 } 1041 1042 private boolean isMenuEmpty(Menu menu) { 1043 boolean retVal = true; 1044 if (menu != null) { 1045 for (MenuItem m : menu.getItems()) { 1046 if (m != null && m.isVisible()) retVal = false; 1047 } 1048 } 1049 return retVal; 1050 } 1051 1052 private void resetOpenMenu() { 1053 if (openMenu != null) { 1054 openMenu.hide(); 1055 openMenu = null; 1056 openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex); 1057 openMenuButton.clearHover(); 1058 openMenuButton = null; 1059 menuModeEnd(); 1060 } 1061 } 1062 1063 private void unSelectMenus() { 1064 clearMenuButtonHover(); 1065 if (focusedMenuIndex == -1) return; 1066 if (openMenu != null) { 1067 openMenu.hide(); 1068 openMenu = null; 1069 } 1070 if (openMenuButton != null) { 1071 openMenuButton.clearHover(); 1072 openMenuButton = null; 1073 } 1074 menuModeEnd(); 1075 } 1076 1077 private void menuModeStart(int newIndex) { 1078 if (focusedMenuIndex == -1) { 1079 SceneHelper.getSceneAccessor().setTransientFocusContainer(getSkinnable().getScene(), getSkinnable()); 1080 } 1081 setFocusedMenuIndex(newIndex); 1082 } 1083 1084 private void menuModeEnd() { 1085 if (focusedMenuIndex != -1) { 1086 SceneHelper.getSceneAccessor().setTransientFocusContainer(getSkinnable().getScene(), null); 1087 1088 /* Return the a11y focus to a control in the scene. */ 1089 getSkinnable().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_NODE); 1090 } 1091 setFocusedMenuIndex(-1); 1092 } 1093 1094 private void moveToMenu(Direction dir, boolean doShow) { 1095 boolean showNextMenu = doShow && focusedMenu.isShowing(); 1096 findSibling(dir, focusedMenuIndex).ifPresent(p -> { 1097 setFocusedMenuIndex(p.getValue()); 1098 if (showNextMenu) { 1099 // we explicitly do *not* allow selection - we are moving 1100 // to a sibling menu, and therefore selection should be reset 1101 showMenu(p.getKey(), false); 1102 } 1103 }); 1104 } 1105 1106 private Optional<Pair<Menu,Integer>> findSibling(Direction dir, int startIndex) { 1107 if (startIndex == -1) { 1108 return Optional.empty(); 1109 } 1110 1111 final int totalMenus = getSkinnable().getMenus().size(); 1112 int i = 0; 1113 int nextIndex = 0; 1114 1115 // Traverse all menus in menubar to find nextIndex 1116 while (i < totalMenus) { 1117 i++; 1118 1119 nextIndex = (startIndex + (dir.isForward() ? 1 : -1)) % totalMenus; 1120 1121 if (nextIndex == -1) { 1122 // loop backwards to end 1123 nextIndex = totalMenus - 1; 1124 } 1125 1126 // if menu at nextIndex is disabled, skip it 1127 if (getSkinnable().getMenus().get(nextIndex).isDisable()) { 1128 // Calculate new nextIndex by continuing loop 1129 startIndex = nextIndex; 1130 } else { 1131 // nextIndex is to be highlighted 1132 break; 1133 } 1134 } 1135 1136 clearMenuButtonHover(); 1137 return Optional.of(new Pair<>(getSkinnable().getMenus().get(nextIndex), nextIndex)); 1138 } 1139 1140 private void updateFocusedIndex() { 1141 int index = 0; 1142 for(Node n : container.getChildren()) { 1143 if (n.isHover()) { 1144 setFocusedMenuIndex(index); 1145 return; 1146 } 1147 index++; 1148 } 1149 menuModeEnd(); 1150 } 1151 1152 private void clearMenuButtonHover() { 1153 for(Node n : container.getChildren()) { 1154 if (n.isHover()) { 1155 ((MenuBarButton)n).clearHover(); 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 }