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