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