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