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