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