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