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