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