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