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