1 /* 2 * Copyright (c) 2011, 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; 27 28 import com.sun.javafx.scene.control.behavior.TwoLevelFocusPopupBehavior; 29 import com.sun.javafx.scene.control.skin.Utils; 30 import javafx.animation.Animation.Status; 31 import javafx.animation.KeyFrame; 32 import javafx.animation.Timeline; 33 import javafx.beans.InvalidationListener; 34 import javafx.beans.WeakInvalidationListener; 35 import javafx.beans.binding.Bindings; 36 import javafx.beans.property.ReadOnlyBooleanProperty; 37 import javafx.beans.value.ChangeListener; 38 import javafx.beans.value.ObservableValue; 39 import javafx.collections.ListChangeListener; 40 import javafx.collections.ObservableList; 41 import javafx.css.CssMetaData; 42 import javafx.css.PseudoClass; 43 import javafx.css.Styleable; 44 import javafx.event.ActionEvent; 45 import javafx.event.EventHandler; 46 import javafx.geometry.*; 47 import javafx.scene.AccessibleAction; 48 import javafx.scene.AccessibleAttribute; 49 import javafx.scene.AccessibleRole; 50 import javafx.scene.Node; 51 import javafx.scene.Parent; 52 import javafx.scene.control.*; 53 import javafx.scene.control.skin.MenuBarSkin; 54 import javafx.scene.input.KeyEvent; 55 import javafx.scene.input.MouseEvent; 56 import javafx.scene.input.ScrollEvent; 57 import javafx.scene.layout.Region; 58 import javafx.scene.layout.StackPane; 59 import javafx.scene.layout.VBox; 60 import javafx.scene.shape.Rectangle; 61 import javafx.stage.Window; 62 import javafx.util.Duration; 63 64 import java.util.ArrayList; 65 import java.util.Collections; 66 import java.util.List; 67 68 /** 69 * This is a the SkinBase for ContextMenu based controls so that the CSS parts 70 * work right, because otherwise we would have to copy the Keys from there to here. 71 */ 72 public class ContextMenuContent extends Region { 73 74 private ContextMenu contextMenu; 75 76 /*************************************************************************** 77 * UI subcomponents 78 **************************************************************************/ 79 80 private double maxGraphicWidth = 0; // we keep this margin to left for graphic 81 private double maxRightWidth = 0; 82 private double maxLabelWidth = 0; 83 private double maxRowHeight = 0; 84 private double maxLeftWidth = 0; 85 private double oldWidth = 0; 86 87 private Rectangle clipRect; 88 MenuBox itemsContainer; 89 private ArrowMenuItem upArrow; 90 private ArrowMenuItem downArrow; 91 92 /* 93 * We maintain a current focused index which is used 94 * in keyboard navigation of menu items. 95 */ 96 private int currentFocusedIndex = -1; 97 98 private boolean itemsDirty = true; 99 private InvalidationListener popupShowingListener = arg0 -> { 100 updateItems(); 101 }; 102 private WeakInvalidationListener weakPopupShowingListener = 103 new WeakInvalidationListener(popupShowingListener); 104 105 /*************************************************************************** 106 * Constructors 107 **************************************************************************/ 108 public ContextMenuContent(final ContextMenu popupMenu) { 109 this.contextMenu = popupMenu; 110 clipRect = new Rectangle(); 111 clipRect.setSmooth(false); 112 itemsContainer = new MenuBox(); 113 // itemsContainer = new VBox(); 114 itemsContainer.setClip(clipRect); 115 116 upArrow = new ArrowMenuItem(this); 117 upArrow.setUp(true); 118 upArrow.setFocusTraversable(false); 119 120 downArrow = new ArrowMenuItem(this); 121 downArrow.setUp(false); 122 downArrow.setFocusTraversable(false); 123 getChildren().add(itemsContainer); 124 getChildren().add(upArrow); 125 getChildren().add(downArrow); 126 initialize(); 127 setUpBinds(); 128 updateItems(); 129 // RT-20197 add menuitems only on first show. 130 popupMenu.showingProperty().addListener(weakPopupShowingListener); 131 132 /* 133 ** only add this if we're on an embedded 134 ** platform that supports 5-button navigation 135 */ 136 if (Utils.isTwoLevelFocus()) { 137 new TwoLevelFocusPopupBehavior(this); 138 } 139 } 140 141 //For access from controls 142 public VBox getItemsContainer() { 143 return itemsContainer; 144 } 145 //For testing purpose only 146 int getCurrentFocusIndex() { 147 return currentFocusedIndex; 148 } 149 //For testing purpose only 150 void setCurrentFocusedIndex(int index) { 151 if (index < itemsContainer.getChildren().size()) { 152 currentFocusedIndex = index; 153 } 154 } 155 156 private void updateItems() { 157 if (itemsDirty) { 158 updateVisualItems(); 159 itemsDirty = false; 160 } 161 } 162 163 private void computeVisualMetrics() { 164 maxRightWidth = 0; 165 maxLabelWidth = 0; 166 maxRowHeight = 0; 167 maxGraphicWidth = 0; 168 maxLeftWidth = 0; 169 170 for (int i = 0; i < itemsContainer.getChildren().size(); i++) { 171 Node child = itemsContainer.getChildren().get(i); 172 if (child instanceof MenuItemContainer) { 173 final MenuItemContainer menuItemContainer = (MenuItemContainer)itemsContainer.getChildren().get(i); 174 175 if (! menuItemContainer.isVisible()) continue; 176 177 double alt = -1; 178 Node n = menuItemContainer.left; 179 if (n != null) { 180 if (n.getContentBias() == Orientation.VERTICAL) { // width depends on height 181 alt = snapSize(n.prefHeight(-1)); 182 } else alt = -1; 183 maxLeftWidth = Math.max(maxLeftWidth, snapSize(n.prefWidth(alt))); 184 maxRowHeight = Math.max(maxRowHeight, n.prefHeight(-1)); 185 } 186 187 n = menuItemContainer.graphic; 188 if (n != null) { 189 if (n.getContentBias() == Orientation.VERTICAL) { // width depends on height 190 alt = snapSize(n.prefHeight(-1)); 191 } else alt = -1; 192 maxGraphicWidth = Math.max(maxGraphicWidth, snapSize(n.prefWidth(alt))); 193 maxRowHeight = Math.max(maxRowHeight, n.prefHeight(-1)); 194 } 195 196 n = menuItemContainer.label; 197 if (n != null) { 198 if (n.getContentBias() == Orientation.VERTICAL) { 199 alt = snapSize(n.prefHeight(-1)); 200 } else alt = -1; 201 maxLabelWidth = Math.max(maxLabelWidth, snapSize(n.prefWidth(alt))); 202 maxRowHeight = Math.max(maxRowHeight, n.prefHeight(-1)); 203 } 204 205 n = menuItemContainer.right; 206 if (n != null) { 207 if (n.getContentBias() == Orientation.VERTICAL) { // width depends on height 208 alt = snapSize(n.prefHeight(-1)); 209 } else alt = -1; 210 maxRightWidth = Math.max(maxRightWidth, snapSize(n.prefWidth(alt))); 211 maxRowHeight = Math.max(maxRowHeight, n.prefHeight(-1)); 212 } 213 } 214 } 215 216 // Fix for RT-38838. 217 // This fixes the issue where CSS is applied to a menu after it has been 218 // showing, resulting in its bounds changing. In this case, we need to 219 // shift the submenu such that it is properly aligned with its parent menu. 220 // 221 // To do this, we must firstly determine if the open submenu is shifted 222 // horizontally to appear on the other side of this menu, as this is the 223 // only situation where shifting has to happen. If so, we need to check 224 // if we should shift the submenu due to changes in width. 225 // 226 // We need to get the parent menu of this contextMenu, so that we only 227 // modify the X value in the following conditions: 228 // 1) There exists a parent menu 229 // 2) The parent menu is in the correct position (i.e. to the left of this 230 // menu in normal LTR systems). 231 final double newWidth = maxRightWidth + maxLabelWidth + maxGraphicWidth + maxLeftWidth; 232 Window ownerWindow = contextMenu.getOwnerWindow(); 233 if (ownerWindow instanceof ContextMenu) { 234 if (contextMenu.getX() < ownerWindow.getX()) { 235 if (oldWidth != newWidth) { 236 contextMenu.setX(contextMenu.getX() + oldWidth - newWidth); 237 } 238 } 239 } 240 241 oldWidth = newWidth; 242 } 243 244 private void updateVisualItems() { 245 ObservableList<Node> itemsContainerChilder = itemsContainer.getChildren(); 246 247 disposeVisualItems(); 248 249 for (int row = 0; row < getItems().size(); row++) { 250 final MenuItem item = getItems().get(row); 251 if (item instanceof CustomMenuItem && ((CustomMenuItem) item).getContent() == null) { 252 continue; 253 } 254 255 if (item instanceof SeparatorMenuItem) { 256 // we don't want the hover highlight for separators, so for 257 // now this is the simplest approach - just remove the 258 // background entirely. This may cause issues if people 259 // intend to style the background differently. 260 Node node = ((CustomMenuItem) item).getContent(); 261 node.visibleProperty().bind(item.visibleProperty()); 262 itemsContainerChilder.add(node); 263 // Add the (separator) menu item to properties map of this node. 264 // Special casing this for separator : 265 // This allows associating this container with SeparatorMenuItem. 266 node.getProperties().put(MenuItem.class, item); 267 } else { 268 MenuItemContainer menuItemContainer = new MenuItemContainer(item); 269 menuItemContainer.visibleProperty().bind(item.visibleProperty()); 270 itemsContainerChilder.add(menuItemContainer); 271 } 272 } 273 274 // Add the Menu to properties map of this skin. Used by QA for testing 275 // This enables associating a parent menu for this skin showing menu items. 276 if (getItems().size() > 0) { 277 final MenuItem item = getItems().get(0); 278 getProperties().put(Menu.class, item.getParentMenu()); 279 } 280 281 // RT-36513 made this applyCss(). Modified by RT-36995 to impl_reapplyCSS() 282 impl_reapplyCSS(); 283 } 284 285 private void disposeVisualItems() { 286 // clean up itemsContainer 287 ObservableList<Node> itemsContainerChilder = itemsContainer.getChildren(); 288 for (int i = 0, max = itemsContainerChilder.size(); i < max; i++) { 289 Node n = itemsContainerChilder.get(i); 290 291 if (n instanceof MenuItemContainer) { 292 MenuItemContainer container = (MenuItemContainer) n; 293 container.visibleProperty().unbind(); 294 container.dispose(); 295 } 296 } 297 itemsContainerChilder.clear(); 298 } 299 300 /** 301 * Can be called by Skins when they need to clean up the content of any 302 * ContextMenu instances they might have created. This ensures that contents 303 * of submenus if any, also get cleaned up. 304 */ 305 public void dispose() { 306 disposeBinds(); 307 disposeVisualItems(); 308 309 disposeContextMenu(submenu); 310 submenu = null; 311 openSubmenu = null; 312 selectedBackground = null; 313 if (contextMenu != null) { 314 contextMenu.getItems().clear(); 315 contextMenu = null; 316 } 317 } 318 319 public void disposeContextMenu(ContextMenu menu) { 320 if (menu == null) return; 321 322 Skin<?> skin = menu.getSkin(); 323 if (skin == null) return; 324 325 ContextMenuContent cmContent = (ContextMenuContent)skin.getNode(); 326 if (cmContent == null) return; 327 328 cmContent.dispose(); // recursive call to dispose submenus. 329 } 330 331 @Override protected void layoutChildren() { 332 if (itemsContainer.getChildren().size() == 0) return; 333 final double x = snappedLeftInset(); 334 final double y = snappedTopInset(); 335 final double w = getWidth() - x - snappedRightInset(); 336 final double h = getHeight() - y - snappedBottomInset(); 337 final double contentHeight = snapSize(getContentHeight()); // itemsContainer.prefHeight(-1); 338 339 itemsContainer.resize(w,contentHeight); 340 itemsContainer.relocate(x, y); 341 342 if (isFirstShow && ty == 0) { 343 upArrow.setVisible(false); 344 isFirstShow = false; 345 } else { 346 upArrow.setVisible(ty < y && ty < 0); 347 } 348 downArrow.setVisible(ty + contentHeight > (y + h)); 349 350 clipRect.setX(0); 351 clipRect.setY(0); 352 clipRect.setWidth(w); 353 clipRect.setHeight(h); 354 355 if (upArrow.isVisible()) { 356 final double prefHeight = snapSize(upArrow.prefHeight(-1)); 357 clipRect.setHeight(snapSize(clipRect.getHeight() - prefHeight)); 358 clipRect.setY(snapSize(clipRect.getY()) + prefHeight); 359 upArrow.resize(snapSize(upArrow.prefWidth(-1)), prefHeight); 360 positionInArea(upArrow, x, y, w, prefHeight, /*baseline ignored*/0, 361 HPos.CENTER, VPos.CENTER); 362 } 363 364 if (downArrow.isVisible()) { 365 final double prefHeight = snapSize(downArrow.prefHeight(-1)); 366 clipRect.setHeight(snapSize(clipRect.getHeight()) - prefHeight); 367 downArrow.resize(snapSize(downArrow.prefWidth(-1)), prefHeight); 368 positionInArea(downArrow, x, (y + h - prefHeight), w, prefHeight, /*baseline ignored*/0, 369 HPos.CENTER, VPos.CENTER); 370 } 371 } 372 373 @Override protected double computePrefWidth(double height) { 374 computeVisualMetrics(); 375 double prefWidth = 0; 376 if (itemsContainer.getChildren().size() == 0) return 0; 377 for (Node n : itemsContainer.getChildren()) { 378 if (! n.isVisible()) continue; 379 prefWidth = Math.max(prefWidth, snapSize(n.prefWidth(-1))); 380 } 381 return snappedLeftInset() + snapSize(prefWidth) + snappedRightInset(); 382 } 383 384 @Override protected double computePrefHeight(double width) { 385 if (itemsContainer.getChildren().size() == 0) return 0; 386 final double screenHeight = getScreenHeight(); 387 final double contentHeight = getContentHeight(); // itemsContainer.prefHeight(width); 388 double totalHeight = snappedTopInset() + snapSize(contentHeight) + snappedBottomInset(); 389 // the pref height of this menu is the smaller value of the 390 // actual pref height and the height of the screens _visual_ bounds. 391 double prefHeight = (screenHeight <= 0) ? (totalHeight) : (Math.min(totalHeight, screenHeight)); 392 return prefHeight; 393 } 394 395 @Override protected double computeMinHeight(double width) { 396 return 0.0; 397 } 398 399 @Override protected double computeMaxHeight(double height) { 400 return getScreenHeight(); 401 } 402 403 private double getScreenHeight() { 404 if (contextMenu == null || contextMenu.getOwnerWindow() == null || 405 contextMenu.getOwnerWindow().getScene() == null) { 406 return -1; 407 } 408 return snapSize(com.sun.javafx.util.Utils.getScreen( 409 contextMenu.getOwnerWindow().getScene().getRoot()).getVisualBounds().getHeight()); 410 411 } 412 413 private double getContentHeight() { 414 double h = 0.0d; 415 for (Node i : itemsContainer.getChildren()) { 416 if (i.isVisible()) { 417 h += snapSize(i.prefHeight(-1)); 418 } 419 } 420 return h; 421 } 422 423 // This handles shifting ty when doing keyboard navigation. 424 private void ensureFocusedMenuItemIsVisible(Node node) { 425 if (node == null) return; 426 427 final Bounds nodeBounds = node.getBoundsInParent(); 428 final Bounds clipBounds = clipRect.getBoundsInParent(); 429 430 if (nodeBounds.getMaxY() >= clipBounds.getMaxY()) { 431 // this is for moving down the menu 432 scroll(-nodeBounds.getMaxY() + clipBounds.getMaxY()); 433 } else if (nodeBounds.getMinY() <= clipBounds.getMinY()) { 434 // this is for moving up the menu 435 scroll(-nodeBounds.getMinY() + clipBounds.getMinY()); 436 } 437 } 438 439 protected ObservableList<MenuItem> getItems() { 440 return contextMenu.getItems(); 441 } 442 443 /** 444 * Finds the index of currently focused item. 445 */ 446 private int findFocusedIndex() { 447 for (int i = 0; i < itemsContainer.getChildren().size(); i++) { 448 Node n = itemsContainer.getChildren().get(i); 449 if (n.isFocused()) { 450 return i; 451 } 452 } 453 return -1; 454 } 455 456 private boolean isFirstShow = true; 457 private double ty; 458 459 private void initialize() { 460 // keyboard navigation support. Initially focus goes to this ContextMenu, 461 // but when the user first hits the up or down arrow keys, the focus 462 // is transferred to the first or last item respectively. Once this 463 // happens, it is up to the menu items to navigate between themselves. 464 contextMenu.focusedProperty().addListener((observable, oldValue, newValue) -> { 465 if (newValue) { 466 // initialize the focused index for keyboard navigation. 467 currentFocusedIndex = -1; 468 requestFocus(); 469 } 470 }); 471 472 // RT-19624 calling requestFocus inside layout was casuing repeated layouts. 473 contextMenu.addEventHandler(Menu.ON_SHOWN, event -> { 474 for (Node child : itemsContainer.getChildren()) { 475 if (child instanceof MenuItemContainer) { 476 final MenuItem item = ((MenuItemContainer)child).item; 477 // When the choiceBox popup is shown, if this menu item is selected 478 // do a requestFocus so CSS kicks in and the item is highlighted. 479 if ("choice-box-menu-item".equals(item.getId())) { 480 if (((RadioMenuItem)item).isSelected()) { 481 child.requestFocus(); 482 break; 483 } 484 } 485 } 486 487 } 488 }); 489 490 // // FIXME For some reason getSkinnable()Behavior traversal functions don't 491 // // get called as expected, so I've just put the important code below. 492 // We use setOnKeyPressed here as we are not adding a listener to a public 493 // event type (ContextMenuContent is not public API), and without this 494 // we get the issue shown in RT-34429 495 setOnKeyPressed(new EventHandler<KeyEvent>() { 496 @Override public void handle(KeyEvent ke) { 497 switch (ke.getCode()) { 498 case LEFT: 499 if (getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) { 500 processRightKey(ke); 501 } else { 502 processLeftKey(ke); 503 } 504 break; 505 case RIGHT: 506 if (getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) { 507 processLeftKey(ke); 508 } else { 509 processRightKey(ke); 510 } 511 break; 512 case CANCEL: 513 ke.consume(); 514 break; 515 case ESCAPE: 516 // if the owner is not a menubar button, just close the 517 // menu - this will move focus up to the parent menu 518 // as required. In the case of the parent being a 519 // menubar button we special case in the conditional code 520 // beneath this switch statement. See RT-34429 for more context. 521 final Node ownerNode = contextMenu.getOwnerNode(); 522 if (! (ownerNode instanceof MenuBarButton)) { 523 contextMenu.hide(); 524 ke.consume(); 525 } 526 break; 527 case DOWN: 528 // move to the next sibling 529 moveToNextSibling(); 530 ke.consume(); 531 break; 532 case UP: 533 // move to previous sibling 534 moveToPreviousSibling(); 535 ke.consume(); 536 break; 537 case SPACE: 538 case ENTER: 539 // select the menuitem 540 selectMenuItem(); 541 ke.consume(); 542 break; 543 default: 544 break; 545 } 546 547 if (!ke.isConsumed()) { 548 final Node ownerNode = contextMenu.getOwnerNode(); 549 if (ownerNode instanceof MenuItemContainer) { 550 // Forward to parent menu 551 Parent parent = ownerNode.getParent(); 552 while (parent != null && !(parent instanceof ContextMenuContent)) { 553 parent = parent.getParent(); 554 } 555 if (parent instanceof ContextMenuContent) { 556 parent.getOnKeyPressed().handle(ke); 557 } 558 } else if (ownerNode instanceof MenuBarButton) { 559 // the following code no longer appears necessary, but 560 // leaving in intact for now... 561 // // This is a top-level MenuBar Menu, so forward event to MenuBar 562 // MenuBarSkin mbs = ((MenuBarButton)ownerNode).getMenuBarSkin(); 563 // if (mbs != null && mbs.getKeyEventHandler() != null) { 564 // mbs.getKeyEventHandler().handle(ke); 565 // } 566 } 567 } 568 } 569 }); 570 571 addEventHandler(ScrollEvent.SCROLL, event -> { 572 /* 573 * we'll only scroll if the arrows are visible in the direction 574 * that we're going, otherwise we go into empty space. 575 */ 576 final double textDeltaY = event.getTextDeltaY(); 577 final double deltaY = event.getDeltaY(); 578 if ((downArrow.isVisible() && (textDeltaY < 0.0 || deltaY < 0.0)) || 579 (upArrow.isVisible() && (textDeltaY > 0.0 || deltaY > 0.0))) { 580 581 switch(event.getTextDeltaYUnits()) { 582 case LINES: 583 /* 584 ** scroll lines, use the row height of selected row, 585 ** or row 0 if none selected 586 */ 587 int focusedIndex = findFocusedIndex(); 588 if (focusedIndex == -1) { 589 focusedIndex = 0; 590 } 591 double rowHeight = itemsContainer.getChildren().get(focusedIndex).prefHeight(-1); 592 scroll(textDeltaY * rowHeight); 593 break; 594 case PAGES: 595 /* 596 ** page scroll, scroll the menu height 597 */ 598 scroll(textDeltaY * itemsContainer.getHeight()); 599 break; 600 case NONE: 601 /* 602 ** pixel scroll 603 */ 604 scroll(deltaY); 605 break; 606 } 607 event.consume(); 608 } 609 }); 610 } 611 612 private void processLeftKey(KeyEvent ke) { 613 if (currentFocusedIndex != -1) { 614 Node n = itemsContainer.getChildren().get(currentFocusedIndex); 615 if (n instanceof MenuItemContainer) { 616 MenuItem item = ((MenuItemContainer)n).item; 617 if (item instanceof Menu) { 618 final Menu menu = (Menu) item; 619 620 // if the submenu for this menu is showing, hide it 621 if (menu == openSubmenu && submenu != null && submenu.isShowing()) { 622 hideSubmenu(); 623 ke.consume(); 624 } 625 } 626 } 627 } 628 } 629 630 private void processRightKey(KeyEvent ke) { 631 if (currentFocusedIndex != -1) { 632 Node n = itemsContainer.getChildren().get(currentFocusedIndex); 633 if (n instanceof MenuItemContainer) { 634 MenuItem item = ((MenuItemContainer)n).item; 635 if (item instanceof Menu) { 636 final Menu menu = (Menu) item; 637 if (menu.isDisable()) return; 638 selectedBackground = ((MenuItemContainer)n); 639 640 // RT-15103 641 // if submenu for this menu is already showing then do nothing 642 // Menubar will process the right key and move to the next menu 643 if (openSubmenu == menu && submenu != null && submenu.isShowing()) { 644 return; 645 } 646 647 showMenu(menu); 648 ke.consume(); 649 } 650 } 651 } 652 } 653 654 private void showMenu(Menu menu) { 655 menu.show(); 656 // request focus on the first item of the submenu after it is shown 657 ContextMenuContent cmContent = (ContextMenuContent)submenu.getSkin().getNode(); 658 if (cmContent != null) { 659 if (cmContent.itemsContainer.getChildren().size() > 0) { 660 cmContent.itemsContainer.getChildren().get(0).requestFocus(); 661 cmContent.currentFocusedIndex = 0; 662 } else { 663 cmContent.requestFocus(); 664 } 665 } 666 } 667 668 private void selectMenuItem() { 669 if (currentFocusedIndex != -1) { 670 Node n = itemsContainer.getChildren().get(currentFocusedIndex); 671 if (n instanceof MenuItemContainer) { 672 MenuItem item = ((MenuItemContainer)n).item; 673 if (item instanceof Menu) { 674 final Menu menu = (Menu) item; 675 if (openSubmenu != null) { 676 hideSubmenu(); 677 } 678 if (menu.isDisable()) return; 679 selectedBackground = ((MenuItemContainer)n); 680 menu.show(); 681 } else { 682 ((MenuItemContainer)n).doSelect(); 683 } 684 } 685 } 686 } 687 /* 688 * Find the index of the next MenuItemContainer in the itemsContainer children. 689 */ 690 private int findNext(int from) { 691 for (int i = from; i < itemsContainer.getChildren().size(); i++) { 692 Node n = itemsContainer.getChildren().get(i); 693 if (n instanceof MenuItemContainer) { 694 return i; 695 } 696 } 697 // find from top 698 for (int i = 0; i < from; i++) { 699 Node n = itemsContainer.getChildren().get(i); 700 if (n instanceof MenuItemContainer) { 701 return i; 702 } 703 } 704 return -1; // should not happen 705 } 706 707 private void moveToNextSibling() { 708 // If focusedIndex is -1 then start from 0th menu item. 709 // Note that this will cycle through such that when you move to last item, 710 // it will move to 1st item on the next Down key press. 711 if (currentFocusedIndex != -1) { 712 currentFocusedIndex = findNext(currentFocusedIndex + 1); 713 } else if (currentFocusedIndex == -1 || currentFocusedIndex == (itemsContainer.getChildren().size() - 1)) { 714 currentFocusedIndex = findNext(0); 715 } 716 717 // request focus on the next sibling which currentFocusIndex points to 718 if (currentFocusedIndex != -1) { 719 Node n = itemsContainer.getChildren().get(currentFocusedIndex); 720 selectedBackground = ((MenuItemContainer)n); 721 n.requestFocus(); 722 ensureFocusedMenuItemIsVisible(n); 723 } 724 } 725 726 /* 727 * Find the index the previous MenuItemContaner in the itemsContainer children. 728 */ 729 private int findPrevious(int from) { 730 for (int i = from; i >= 0; i--) { 731 Node n = itemsContainer.getChildren().get(i); 732 if (n instanceof MenuItemContainer) { 733 return(i); 734 } 735 } 736 for (int i = itemsContainer.getChildren().size() - 1 ; i > from; i--) { 737 Node n = itemsContainer.getChildren().get(i); 738 if (n instanceof MenuItemContainer) { 739 return(i); 740 } 741 } 742 return -1; 743 } 744 745 private void moveToPreviousSibling() { 746 // If focusedIndex is -1 then start from the last menu item to go up. 747 // Note that this will cycle through such that when you move to first item, 748 // it will move to last item on the next Up key press. 749 if (currentFocusedIndex != -1) { 750 currentFocusedIndex = findPrevious(currentFocusedIndex - 1); 751 } else if(currentFocusedIndex == -1 || currentFocusedIndex == 0) { 752 currentFocusedIndex = findPrevious(itemsContainer.getChildren().size() - 1); 753 } 754 755 // request focus on the previous sibling which currentFocusIndex points to 756 if (currentFocusedIndex != -1) { 757 Node n = itemsContainer.getChildren().get(currentFocusedIndex); 758 selectedBackground = ((MenuItemContainer)n); 759 n.requestFocus(); 760 ensureFocusedMenuItemIsVisible(n); 761 } 762 } 763 764 /* 765 * Get the Y offset from the top of the popup to the menu item whose index 766 * is given. 767 */ 768 public double getMenuYOffset(int menuIndex) { 769 double offset = 0; 770 if (itemsContainer.getChildren().size() > menuIndex) { 771 offset = snappedTopInset(); 772 Node menuitem = itemsContainer.getChildren().get(menuIndex); 773 offset += menuitem.getLayoutY() + menuitem.prefHeight(-1); 774 } 775 return offset; 776 } 777 778 private void setUpBinds() { 779 updateMenuShowingListeners(contextMenu.getItems(), true); 780 contextMenu.getItems().addListener(contextMenuItemsListener); 781 } 782 783 private void disposeBinds() { 784 updateMenuShowingListeners(contextMenu.getItems(), false); 785 contextMenu.getItems().removeListener(contextMenuItemsListener); 786 } 787 788 private ChangeListener<Boolean> menuShowingListener = (observable, wasShowing, isShowing) -> { 789 ReadOnlyBooleanProperty isShowingProperty = (ReadOnlyBooleanProperty) observable; 790 Menu menu = (Menu) isShowingProperty.getBean(); 791 792 if (wasShowing && ! isShowing) { 793 // hide the submenu popup 794 hideSubmenu(); 795 } else if (! wasShowing && isShowing) { 796 // show the submenu popup 797 showSubmenu(menu); 798 } 799 }; 800 801 private ListChangeListener<MenuItem> contextMenuItemsListener = (ListChangeListener<MenuItem>) c -> { 802 // Add listeners to the showing property of all menus that have 803 // been added, and remove listeners from menus that have been removed 804 // FIXME this is temporary - we should be adding and removing 805 // listeners such that they use the one listener defined above 806 // - but that can't be done until we have the bean in the 807 // ObservableValue 808 while (c.next()) { 809 updateMenuShowingListeners(c.getRemoved(), false); 810 updateMenuShowingListeners(c.getAddedSubList(), true); 811 } 812 813 // Listener to items in PopupMenu to update items in PopupMenuContent 814 itemsDirty = true; 815 updateItems(); // RT-29761 816 }; 817 818 private ChangeListener<Boolean> menuItemVisibleListener = (observable, oldValue, newValue) -> { 819 // re layout as item's visibility changed 820 requestLayout(); 821 }; 822 823 private void updateMenuShowingListeners(List<? extends MenuItem> items, boolean addListeners) { 824 for (MenuItem item : items) { 825 if (item instanceof Menu) { 826 final Menu menu = (Menu) item; 827 828 if (addListeners) { 829 menu.showingProperty().addListener(menuShowingListener); 830 } else { 831 menu.showingProperty().removeListener(menuShowingListener); 832 } 833 } 834 835 // listen to menu items's visible property. 836 if (addListeners) { 837 item.visibleProperty().addListener(menuItemVisibleListener); 838 } else { 839 item.visibleProperty().removeListener(menuItemVisibleListener); 840 } 841 } 842 } 843 844 // For test purpose only 845 ContextMenu getSubMenu() { 846 return submenu; 847 } 848 849 Menu getOpenSubMenu() { 850 return openSubmenu; 851 } 852 853 private void createSubmenu() { 854 if (submenu == null) { 855 submenu = new ContextMenu(); 856 submenu.showingProperty().addListener(new ChangeListener<Boolean>() { 857 @Override public void changed(ObservableValue<? extends Boolean> observable, 858 Boolean oldValue, Boolean newValue) { 859 if (!submenu.isShowing()) { 860 // Maybe user clicked outside or typed ESCAPE. 861 // Make sure menus are in sync. 862 for (Node node : itemsContainer.getChildren()) { 863 if (node instanceof MenuItemContainer 864 && ((MenuItemContainer)node).item instanceof Menu) { 865 Menu menu = (Menu)((MenuItemContainer)node).item; 866 if (menu.isShowing()) { 867 menu.hide(); 868 } 869 } 870 } 871 } 872 } 873 }); 874 } 875 } 876 877 private void showSubmenu(Menu menu) { 878 openSubmenu = menu; 879 createSubmenu(); 880 submenu.getItems().setAll(menu.getItems()); 881 submenu.show(selectedBackground, Side.RIGHT, 0, 0); 882 } 883 884 private void hideSubmenu() { 885 if (submenu == null) return; 886 887 submenu.hide(); 888 openSubmenu = null; 889 890 // Fix for RT-37022 - we dispose content so that we do not process CSS 891 // on hidden submenus 892 disposeContextMenu(submenu); 893 submenu = null; 894 } 895 896 private void hideAllMenus(MenuItem item) { 897 if (contextMenu != null) contextMenu.hide(); 898 899 Menu parentMenu; 900 while ((parentMenu = item.getParentMenu()) != null) { 901 parentMenu.hide(); 902 item = parentMenu; 903 } 904 if (item.getParentPopup() != null) { 905 item.getParentPopup().hide(); 906 } 907 } 908 909 private Menu openSubmenu; 910 private ContextMenu submenu; 911 912 // FIXME: HACKY. We use this so that a submenu knows where to open from 913 // but this will only work for mouse hovers currently - and won't work 914 // programmatically. 915 // package protected for testing only! 916 Region selectedBackground; 917 918 void scroll(double delta) { 919 double newTy = ty + delta; 920 if (ty == newTy) return; 921 922 // translation should never be positive (this would mean the top of the 923 // menu content is detaching from the top of the menu!) 924 if (newTy > 0.0) { 925 newTy = 0.0; 926 } 927 928 // translation should never be greater than the preferred height of the 929 // menu content (otherwise the menu content will be detaching from the 930 // bottom of the menu). 931 // RT-37185: We check the direction of the scroll, to prevent it locking 932 // up when scrolling upwards from the very bottom (using the on-screen 933 // up arrow). 934 if (delta < 0 && (getHeight() - newTy) > itemsContainer.getHeight() - downArrow.getHeight()) { 935 newTy = getHeight() - itemsContainer.getHeight() - downArrow.getHeight(); 936 } 937 938 ty = newTy; 939 itemsContainer.requestLayout(); 940 } 941 942 /*************************************************************************** 943 * * 944 * Stylesheet Handling * 945 * * 946 **************************************************************************/ 947 @Override public Styleable getStyleableParent() { 948 return contextMenu; 949 } 950 951 /** @treatAsPrivate */ 952 private static class StyleableProperties { 953 954 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 955 static { 956 957 final List<CssMetaData<? extends Styleable, ?>> styleables = 958 new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData()); 959 960 // 961 // SkinBase only has Region's unique StlyleableProperty's, none of Nodes 962 // So, we need to add effect back in. The effect property is in a 963 // private inner class, so get the property from Node the hard way. 964 final List<CssMetaData<? extends Styleable, ?>> nodeStyleables = Node.getClassCssMetaData(); 965 for(int n=0, max=nodeStyleables.size(); n<max; n++) { 966 CssMetaData<? extends Styleable, ?> styleable = nodeStyleables.get(n); 967 if ("effect".equals(styleable.getProperty())) { 968 styleables.add(styleable); 969 break; 970 } 971 } 972 STYLEABLES = Collections.unmodifiableList(styleables); 973 } 974 } 975 976 /** 977 * @return The CssMetaData associated with this class, which may include the 978 * CssMetaData of its super classes. 979 */ 980 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 981 return StyleableProperties.STYLEABLES; 982 } 983 984 /** 985 * {@inheritDoc} 986 */ 987 @Override 988 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 989 return getClassCssMetaData(); 990 } 991 992 public Label getLabelAt(int index) { 993 return ((MenuItemContainer)itemsContainer.getChildren().get(index)).getLabel(); 994 } 995 996 /** 997 * Custom VBox to enable scrolling of items. Scrolling effect is achieved by 998 * controlling the translate Y coordinate of the menu item "ty" which is set by a 999 * timeline when mouse is over up/down arrow. 1000 */ 1001 class MenuBox extends VBox { 1002 MenuBox() { 1003 setAccessibleRole(AccessibleRole.CONTEXT_MENU); 1004 } 1005 1006 @Override protected void layoutChildren() { 1007 double yOffset = ty; 1008 for (Node n : getChildren()) { 1009 if (n.isVisible()) { 1010 final double prefHeight = snapSize(n.prefHeight(-1)); 1011 n.resize(snapSize(getWidth()), prefHeight); 1012 n.relocate(snappedLeftInset(), yOffset); 1013 yOffset += prefHeight; 1014 } 1015 } 1016 } 1017 1018 @Override 1019 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1020 switch (attribute) { 1021 case VISIBLE: return contextMenu.isShowing(); 1022 case PARENT_MENU: return contextMenu.getOwnerNode(); 1023 default: return super.queryAccessibleAttribute(attribute, parameters); 1024 } 1025 } 1026 } 1027 1028 class ArrowMenuItem extends StackPane { 1029 private StackPane upDownArrow; 1030 private ContextMenuContent popupMenuContent; 1031 private boolean up = false; 1032 public final boolean isUp() { return up; } 1033 public void setUp(boolean value) { 1034 up = value; 1035 upDownArrow.getStyleClass().setAll(isUp() ? "menu-up-arrow" : "menu-down-arrow"); 1036 } 1037 1038 // used to automatically scroll through menu items when the user performs 1039 // certain interactions, e.g. pressing and holding the arrow buttons 1040 private Timeline scrollTimeline; 1041 1042 public ArrowMenuItem(ContextMenuContent pmc) { 1043 getStyleClass().setAll("scroll-arrow"); 1044 upDownArrow = new StackPane(); 1045 this.popupMenuContent = pmc; 1046 upDownArrow.setMouseTransparent(true); 1047 upDownArrow.getStyleClass().setAll(isUp() ? "menu-up-arrow" : "menu-down-arrow"); 1048 // setMaxWidth(Math.max(upDownArrow.prefWidth(-1), getWidth())); 1049 addEventHandler(MouseEvent.MOUSE_ENTERED, me -> { 1050 if (scrollTimeline != null && (scrollTimeline.getStatus() != Status.STOPPED)) { 1051 return; 1052 } 1053 startTimeline(); 1054 }); 1055 addEventHandler(MouseEvent.MOUSE_EXITED, me -> { 1056 stopTimeline(); 1057 }); 1058 setVisible(false); 1059 setManaged(false); 1060 getChildren().add(upDownArrow); 1061 } 1062 1063 @Override protected double computePrefWidth(double height) { 1064 // return snapSize(getInsets().getLeft()) + snapSize(getInsets().getRight()); 1065 return itemsContainer.getWidth(); 1066 } 1067 1068 @Override protected double computePrefHeight(double width) { 1069 return snappedTopInset() + upDownArrow.prefHeight(-1) + snappedBottomInset(); 1070 } 1071 1072 @Override protected void layoutChildren() { 1073 double w = snapSize(upDownArrow.prefWidth(-1)); 1074 double h = snapSize(upDownArrow.prefHeight(-1)); 1075 1076 upDownArrow.resize(w, h); 1077 positionInArea(upDownArrow, 0, 0, getWidth(), getHeight(), 1078 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1079 } 1080 1081 private void adjust() { 1082 if(up) popupMenuContent.scroll(12); else popupMenuContent.scroll(-12); 1083 } 1084 1085 private void startTimeline() { 1086 scrollTimeline = new Timeline(); 1087 scrollTimeline.setCycleCount(Timeline.INDEFINITE); 1088 KeyFrame kf = new KeyFrame( 1089 Duration.millis(60), 1090 event -> { 1091 adjust(); 1092 } 1093 ); 1094 scrollTimeline.getKeyFrames().clear(); 1095 scrollTimeline.getKeyFrames().add(kf); 1096 scrollTimeline.play(); 1097 } 1098 1099 private void stopTimeline() { 1100 scrollTimeline.stop(); 1101 scrollTimeline = null; 1102 } 1103 } 1104 1105 /* 1106 * Container responsible for laying out a single row in the menu - in other 1107 * words, this contains and lays out a single MenuItem, regardless of it's 1108 * specific subtype. 1109 */ 1110 public class MenuItemContainer extends Region { 1111 1112 private final MenuItem item; 1113 1114 private Node left; 1115 private Node graphic; 1116 private Node label; 1117 private Node right; 1118 1119 private final LambdaMultiplePropertyChangeListenerHandler listener = 1120 new LambdaMultiplePropertyChangeListenerHandler(); 1121 1122 private EventHandler<MouseEvent> mouseEnteredEventHandler; 1123 private EventHandler<MouseEvent> mouseReleasedEventHandler; 1124 1125 private EventHandler<ActionEvent> actionEventHandler; 1126 1127 protected Label getLabel(){ 1128 return (Label) label; 1129 } 1130 1131 public MenuItem getItem() { 1132 return item; 1133 } 1134 1135 public MenuItemContainer(MenuItem item){ 1136 if (item == null) { 1137 throw new NullPointerException("MenuItem can not be null"); 1138 } 1139 1140 getStyleClass().addAll(item.getStyleClass()); 1141 setId(item.getId()); 1142 setFocusTraversable(!(item instanceof CustomMenuItem)); 1143 this.item = item; 1144 1145 createChildren(); 1146 1147 // listen to changes in the state of certain MenuItem types 1148 ReadOnlyBooleanProperty pseudoProperty; 1149 if (item instanceof Menu) { 1150 pseudoProperty = ((Menu)item).showingProperty(); 1151 listener.registerChangeListener(pseudoProperty, 1152 e -> pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, ((Menu) item).isShowing())); 1153 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, pseudoProperty.get()); 1154 setAccessibleRole(AccessibleRole.MENU); 1155 } else if (item instanceof RadioMenuItem) { 1156 pseudoProperty = ((RadioMenuItem)item).selectedProperty(); 1157 listener.registerChangeListener(pseudoProperty, 1158 e -> pseudoClassStateChanged(CHECKED_PSEUDOCLASS_STATE, ((RadioMenuItem) item).isSelected())); 1159 pseudoClassStateChanged(CHECKED_PSEUDOCLASS_STATE, pseudoProperty.get()); 1160 setAccessibleRole(AccessibleRole.RADIO_MENU_ITEM); 1161 } else if (item instanceof CheckMenuItem) { 1162 pseudoProperty = ((CheckMenuItem)item).selectedProperty(); 1163 listener.registerChangeListener(pseudoProperty, 1164 e -> pseudoClassStateChanged(CHECKED_PSEUDOCLASS_STATE, ((CheckMenuItem) item).isSelected())); 1165 pseudoClassStateChanged(CHECKED_PSEUDOCLASS_STATE, pseudoProperty.get()); 1166 setAccessibleRole(AccessibleRole.CHECK_MENU_ITEM); 1167 } else { 1168 setAccessibleRole(AccessibleRole.MENU_ITEM); 1169 } 1170 1171 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, item.disableProperty().get()); 1172 listener.registerChangeListener(item.disableProperty(), 1173 e -> pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, item.isDisable())); 1174 1175 // Add the menu item to properties map of this node. Used by QA for testing 1176 // This allows associating this container with corresponding MenuItem. 1177 getProperties().put(MenuItem.class, item); 1178 1179 listener.registerChangeListener(item.graphicProperty(), e -> { 1180 createChildren(); 1181 computeVisualMetrics(); 1182 }); 1183 1184 actionEventHandler = e -> { 1185 if (item instanceof Menu) { 1186 final Menu menu = (Menu) item; 1187 if (openSubmenu == menu && submenu.isShowing()) return; 1188 if (openSubmenu != null) { 1189 hideSubmenu(); 1190 } 1191 1192 selectedBackground = MenuItemContainer.this; 1193 showMenu(menu); 1194 } else { 1195 doSelect(); 1196 } 1197 }; 1198 addEventHandler(ActionEvent.ACTION, actionEventHandler); 1199 } 1200 1201 public void dispose() { 1202 if (item instanceof CustomMenuItem) { 1203 Node node = ((CustomMenuItem)item).getContent(); 1204 if (node != null) { 1205 node.removeEventHandler(MouseEvent.MOUSE_CLICKED, customMenuItemMouseClickedHandler); 1206 } 1207 } 1208 1209 listener.dispose(); 1210 removeEventHandler(ActionEvent.ACTION, actionEventHandler); 1211 1212 if (label != null) { 1213 ((Label)label).textProperty().unbind(); 1214 label.styleProperty().unbind(); 1215 label.idProperty().unbind(); 1216 Bindings.unbindContent(label.getStyleClass(), item.getStyleClass()); 1217 } 1218 1219 left = null; 1220 graphic = null; 1221 label = null; 1222 right = null; 1223 } 1224 1225 private void createChildren() { 1226 getChildren().clear(); 1227 1228 // draw background region for hover effects. All content (other 1229 // than Nodes from NodeMenuItems) are set to be mouseTransparent, so 1230 // this background also acts as the receiver of user input 1231 if (item instanceof CustomMenuItem) { 1232 createNodeMenuItemChildren((CustomMenuItem)item); 1233 1234 if (mouseEnteredEventHandler == null) { 1235 mouseEnteredEventHandler = event -> { 1236 requestFocus(); // request Focus on hover 1237 }; 1238 } else { 1239 removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler); 1240 } 1241 addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler); 1242 } else { 1243 // --- add check / radio to left column 1244 Node leftNode = getLeftGraphic(item); 1245 if (leftNode != null) { 1246 StackPane leftPane = new StackPane(); 1247 leftPane.getStyleClass().add("left-container"); 1248 leftPane.getChildren().add(leftNode); 1249 left = leftPane; 1250 getChildren().add(left); 1251 left.setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); 1252 } 1253 // -- add graphic to graphic pane 1254 if (item.getGraphic() != null) { 1255 Node graphicNode = item.getGraphic(); 1256 StackPane graphicPane = new StackPane(); 1257 graphicPane.getStyleClass().add("graphic-container"); 1258 graphicPane.getChildren().add(graphicNode); 1259 graphic = graphicPane; 1260 getChildren().add(graphic); 1261 } 1262 1263 // --- add text to center column 1264 label = new MenuLabel(item, this); // make this a menulabel to handle mnemonics fire() 1265 1266 // set up bindings from the MenuItem into the Label 1267 ((Label)label).textProperty().bind(item.textProperty()); 1268 label.styleProperty().bind(item.styleProperty()); 1269 label.idProperty().bind(item.styleProperty()); 1270 Bindings.bindContent(label.getStyleClass(), item.getStyleClass()); 1271 1272 1273 label.setMouseTransparent(true); 1274 getChildren().add(label); 1275 1276 listener.unregisterChangeListener(focusedProperty()); 1277 // RT-19546 update currentFocusedIndex when MenuItemContainer gets focused. 1278 // e.g this happens when you press the Right key to open a submenu; the first 1279 // menuitem is focused. 1280 listener.registerChangeListener(focusedProperty(), e -> { 1281 if (isFocused()) { 1282 currentFocusedIndex = itemsContainer.getChildren().indexOf(MenuItemContainer.this); 1283 } 1284 }); 1285 1286 // --- draw in right column - this depends on whether we are 1287 // a Menu or not. A Menu gets an arrow, whereas other MenuItems 1288 // get the ability to draw an accelerator 1289 if (item instanceof Menu) { 1290 // --- add arrow / accelerator / mnemonic to right column 1291 Region rightNode = new Region(); 1292 rightNode.setMouseTransparent(true); 1293 rightNode.getStyleClass().add("arrow"); 1294 1295 StackPane rightPane = new StackPane(); 1296 rightPane.setMaxWidth(Math.max(rightNode.prefWidth(-1), 10)); 1297 rightPane.setMouseTransparent(true); 1298 rightPane.getStyleClass().add("right-container"); 1299 rightPane.getChildren().add(rightNode); 1300 right = rightPane; 1301 getChildren().add(rightPane); 1302 1303 if (mouseEnteredEventHandler == null) { 1304 mouseEnteredEventHandler = event -> { 1305 if (openSubmenu != null && item != openSubmenu) { 1306 // if a submenu of a different menu is already 1307 // open then close it (RT-15049) 1308 hideSubmenu(); 1309 } 1310 1311 final Menu menu = (Menu) item; 1312 if (menu.isDisable()) return; 1313 selectedBackground = MenuItemContainer.this; 1314 menu.show(); 1315 requestFocus(); // request Focus on hover 1316 }; 1317 } else { 1318 removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler); 1319 } 1320 1321 if (mouseReleasedEventHandler == null) { 1322 mouseReleasedEventHandler = event -> { 1323 item.fire(); 1324 }; 1325 } else { 1326 removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler); 1327 } 1328 1329 // show submenu when the menu is hovered over 1330 addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler); 1331 addEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler); 1332 } else { // normal MenuItem 1333 // remove old listeners 1334 listener.unregisterChangeListener(item.acceleratorProperty()); 1335 1336 // accelerator support 1337 updateAccelerator(); 1338 1339 if (mouseEnteredEventHandler == null) { 1340 mouseEnteredEventHandler = event -> { 1341 if (openSubmenu != null) { 1342 openSubmenu.hide(); 1343 } 1344 requestFocus(); // request Focus on hover 1345 }; 1346 } else { 1347 removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler); 1348 } 1349 1350 if (mouseReleasedEventHandler == null) { 1351 mouseReleasedEventHandler = event -> { 1352 doSelect(); 1353 }; 1354 } else { 1355 removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler); 1356 } 1357 1358 addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler); 1359 addEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler); 1360 1361 listener.registerChangeListener(item.acceleratorProperty(), e -> updateAccelerator()); 1362 } 1363 } 1364 } 1365 1366 private void updateAccelerator() { 1367 if (item.getAccelerator() != null) { 1368 if (right != null) { 1369 getChildren().remove(right); 1370 } 1371 1372 String text = item.getAccelerator().getDisplayText(); 1373 right = new Label(text); 1374 right.setStyle(item.getStyle()); 1375 right.getStyleClass().add("accelerator-text"); 1376 getChildren().add(right); 1377 } else { 1378 getChildren().remove(right); 1379 } 1380 } 1381 1382 void doSelect() { 1383 // don't do anything on disabled menu items 1384 if (item.isDisable()) return; 1385 // toggle state of check or radio items 1386 if (item instanceof CheckMenuItem) { 1387 CheckMenuItem checkItem = (CheckMenuItem)item; 1388 checkItem.setSelected(!checkItem.isSelected()); 1389 } else if (item instanceof RadioMenuItem) { 1390 // this is a radio button. If there is a toggleGroup specified, we 1391 // simply set selected to true. If no toggleGroup is specified, we 1392 // toggle the selected state, as there is no assumption of mutual 1393 // exclusivity when no toggleGroup is set. 1394 final RadioMenuItem radioItem = (RadioMenuItem) item; 1395 radioItem.setSelected(radioItem.getToggleGroup() != null ? true : !radioItem.isSelected()); 1396 } 1397 1398 // fire the action before hiding the menu 1399 item.fire(); 1400 1401 if (item instanceof CustomMenuItem) { 1402 CustomMenuItem customMenuItem = (CustomMenuItem) item; 1403 if (customMenuItem.isHideOnClick()) { 1404 hideAllMenus(item); 1405 } 1406 } else { 1407 hideAllMenus(item); 1408 } 1409 } 1410 1411 private EventHandler<MouseEvent> customMenuItemMouseClickedHandler; 1412 1413 private void createNodeMenuItemChildren(final CustomMenuItem item) { 1414 Node node = item.getContent(); 1415 getChildren().add(node); 1416 1417 // handle hideOnClick 1418 customMenuItemMouseClickedHandler = event -> { 1419 if (item == null || item.isDisable()) return; 1420 1421 item.fire(); 1422 if (item.isHideOnClick()) { 1423 hideAllMenus(item); 1424 } 1425 }; 1426 node.addEventHandler(MouseEvent.MOUSE_CLICKED, customMenuItemMouseClickedHandler); 1427 } 1428 1429 @Override protected void layoutChildren() { 1430 double xOffset; 1431 1432 final double prefHeight = prefHeight(-1); 1433 if (left != null) { 1434 xOffset = snappedLeftInset(); 1435 left.resize(left.prefWidth(-1), left.prefHeight(-1)); 1436 positionInArea(left, xOffset, 0, 1437 maxLeftWidth, prefHeight, 0, HPos.LEFT, VPos.CENTER); 1438 } 1439 if (graphic != null) { 1440 xOffset = snappedLeftInset() + maxLeftWidth; 1441 graphic.resize(graphic.prefWidth(-1), graphic.prefHeight(-1)); 1442 positionInArea(graphic, xOffset, 0, 1443 maxGraphicWidth, prefHeight, 0, HPos.LEFT, VPos.CENTER); 1444 } 1445 1446 if (label != null) { 1447 xOffset = snappedLeftInset() + maxLeftWidth + maxGraphicWidth; 1448 label.resize(label.prefWidth(-1), label.prefHeight(-1)); 1449 positionInArea(label, xOffset, 0, 1450 maxLabelWidth, prefHeight, 0, HPos.LEFT, VPos.CENTER); 1451 } 1452 1453 if (right != null) { 1454 xOffset = snappedLeftInset() + maxLeftWidth + maxGraphicWidth + maxLabelWidth; 1455 right.resize(right.prefWidth(-1), right.prefHeight(-1)); 1456 positionInArea(right, xOffset, 0, 1457 maxRightWidth, prefHeight, 0, HPos.RIGHT, VPos.CENTER); 1458 } 1459 1460 if ( item instanceof CustomMenuItem) { 1461 Node n = ((CustomMenuItem) item).getContent(); 1462 if (item instanceof SeparatorMenuItem) { 1463 double width = prefWidth(-1) - (snappedLeftInset() + maxGraphicWidth + snappedRightInset()); 1464 n.resize(width, n.prefHeight(-1)); 1465 positionInArea(n, snappedLeftInset() + maxGraphicWidth, 0, prefWidth(-1), prefHeight, 0, HPos.LEFT, VPos.CENTER); 1466 } else { 1467 n.resize(n.prefWidth(-1), n.prefHeight(-1)); 1468 //the node should be left aligned 1469 positionInArea(n, snappedLeftInset(), 0, getWidth(), prefHeight, 0, HPos.LEFT, VPos.CENTER); 1470 } 1471 } 1472 } 1473 1474 @Override protected double computePrefHeight(double width) { 1475 double prefHeight = 0; 1476 if (item instanceof CustomMenuItem || item instanceof SeparatorMenuItem) { 1477 prefHeight = (getChildren().isEmpty()) ? 0 : getChildren().get(0).prefHeight(-1); 1478 } else { 1479 prefHeight = Math.max(prefHeight, (left != null) ? left.prefHeight(-1) : 0); 1480 prefHeight = Math.max(prefHeight, (graphic != null) ? graphic.prefHeight(-1) : 0); 1481 prefHeight = Math.max(prefHeight, (label != null) ? label.prefHeight(-1) : 0); 1482 prefHeight = Math.max(prefHeight, (right != null) ? right.prefHeight(-1) : 0); 1483 } 1484 return snappedTopInset() + prefHeight + snappedBottomInset(); 1485 } 1486 1487 @Override protected double computePrefWidth(double height) { 1488 double nodeMenuItemWidth = 0; 1489 if (item instanceof CustomMenuItem && !(item instanceof SeparatorMenuItem)) { 1490 nodeMenuItemWidth = snappedLeftInset() + ((CustomMenuItem) item).getContent().prefWidth(-1) + 1491 snappedRightInset(); 1492 } 1493 return Math.max(nodeMenuItemWidth, 1494 snappedLeftInset() + maxLeftWidth + maxGraphicWidth + 1495 maxLabelWidth + maxRightWidth + snappedRightInset()); 1496 } 1497 1498 // Responsible for returning a graphic (if necessary) to position in the 1499 // left column of the menu. This may be a Node from the MenuItem.graphic 1500 // property, or it may be a check/radio item if necessary. 1501 private Node getLeftGraphic(MenuItem item) { 1502 if (item instanceof RadioMenuItem) { 1503 final Region _graphic = new Region(); 1504 _graphic.getStyleClass().add("radio"); 1505 return _graphic; 1506 } else if (item instanceof CheckMenuItem) { 1507 final StackPane _graphic = new StackPane(); 1508 _graphic.getStyleClass().add("check"); 1509 return _graphic; 1510 } 1511 1512 return null; 1513 } 1514 1515 @Override 1516 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1517 switch (attribute) { 1518 case SELECTED: 1519 if (item instanceof CheckMenuItem) { 1520 return ((CheckMenuItem)item).isSelected(); 1521 } 1522 if (item instanceof RadioMenuItem) { 1523 return ((RadioMenuItem) item).isSelected(); 1524 } 1525 return false; 1526 case ACCELERATOR: return item.getAccelerator(); 1527 case TEXT: { 1528 String title = ""; 1529 if (graphic != null) { 1530 String t = (String)graphic.queryAccessibleAttribute(AccessibleAttribute.TEXT); 1531 if (t != null) title += t; 1532 } 1533 final Label label = getLabel(); 1534 if (label != null) { 1535 String t = (String)label.queryAccessibleAttribute(AccessibleAttribute.TEXT); 1536 if (t != null) title += t; 1537 } 1538 if (item instanceof CustomMenuItem) { 1539 Node content = ((CustomMenuItem) item).getContent(); 1540 if (content != null) { 1541 String t = (String)content.queryAccessibleAttribute(AccessibleAttribute.TEXT); 1542 if (t != null) title += t; 1543 } 1544 } 1545 return title; 1546 } 1547 case MNEMONIC: { 1548 final Label label = getLabel(); 1549 if (label != null) { 1550 String mnemonic = (String)label.queryAccessibleAttribute(AccessibleAttribute.MNEMONIC); 1551 if (mnemonic != null) return mnemonic; 1552 } 1553 return null; 1554 } 1555 case DISABLED: return item.isDisable(); 1556 case SUBMENU: 1557 createSubmenu(); 1558 // Accessibility might need to see the menu node before the window 1559 // is visible (i.e. before the skin is applied). 1560 if (submenu.getSkin() == null) { 1561 submenu.getStyleableNode().impl_processCSS(true); 1562 } 1563 ContextMenuContent cmContent = (ContextMenuContent)submenu.getSkin().getNode(); 1564 return cmContent.itemsContainer; 1565 default: return super.queryAccessibleAttribute(attribute, parameters); 1566 } 1567 } 1568 1569 @Override 1570 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 1571 switch (action) { 1572 case SHOW_MENU:{ 1573 if (item instanceof Menu) { 1574 final Menu menuItem = (Menu) item; 1575 if (menuItem.isShowing()) { 1576 menuItem.hide(); 1577 } else { 1578 menuItem.show(); 1579 } 1580 } 1581 break; 1582 } 1583 case FIRE: 1584 doSelect(); 1585 break; 1586 default: super.executeAccessibleAction(action); 1587 } 1588 } 1589 } 1590 1591 1592 private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = 1593 PseudoClass.getPseudoClass("selected"); 1594 private static final PseudoClass DISABLED_PSEUDOCLASS_STATE = 1595 PseudoClass.getPseudoClass("disabled"); 1596 private static final PseudoClass CHECKED_PSEUDOCLASS_STATE = 1597 PseudoClass.getPseudoClass("checked"); 1598 1599 private class MenuLabel extends Label { 1600 1601 public MenuLabel(MenuItem item, MenuItemContainer mic) { 1602 super(item.getText()); 1603 setMnemonicParsing(item.isMnemonicParsing()); 1604 setLabelFor(mic); 1605 } 1606 } 1607 1608 }