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