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