1 /*
   2  * Copyright (c) 2010, 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 javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.ParentHelper;
  29 import java.util.ArrayList;
  30 import java.util.Collections;
  31 import java.util.List;
  32 
  33 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  34 import com.sun.javafx.scene.traversal.Algorithm;
  35 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
  36 import com.sun.javafx.scene.traversal.TraversalContext;
  37 
  38 import javafx.beans.property.ObjectProperty;
  39 import javafx.beans.property.DoubleProperty;
  40 import javafx.beans.value.WritableValue;
  41 import javafx.collections.FXCollections;
  42 import javafx.collections.ListChangeListener;
  43 import javafx.collections.ObservableList;
  44 import javafx.geometry.HPos;
  45 import javafx.geometry.Orientation;
  46 import javafx.geometry.Pos;
  47 import javafx.geometry.Side;
  48 import javafx.geometry.VPos;
  49 import javafx.scene.AccessibleAction;
  50 import javafx.scene.AccessibleAttribute;
  51 import javafx.scene.AccessibleRole;
  52 import javafx.scene.Node;
  53 import javafx.scene.Parent;
  54 import javafx.scene.control.ContextMenu;
  55 import javafx.scene.control.Control;
  56 import javafx.scene.control.MenuItem;
  57 import javafx.scene.control.CustomMenuItem;
  58 import javafx.scene.control.Separator;
  59 import javafx.scene.control.SeparatorMenuItem;
  60 import javafx.scene.control.SkinBase;
  61 import javafx.scene.control.ToolBar;
  62 import javafx.scene.input.KeyCode;
  63 import javafx.scene.layout.HBox;
  64 import javafx.scene.layout.Pane;
  65 import javafx.scene.layout.StackPane;
  66 import javafx.scene.layout.VBox;
  67 import javafx.css.StyleableDoubleProperty;
  68 import javafx.css.StyleableObjectProperty;
  69 import javafx.css.StyleableProperty;
  70 import javafx.css.CssMetaData;
  71 
  72 import javafx.css.converter.EnumConverter;
  73 import javafx.css.converter.SizeConverter;
  74 import com.sun.javafx.scene.control.behavior.ToolBarBehavior;
  75 import com.sun.javafx.scene.traversal.Direction;
  76 
  77 import javafx.css.Styleable;
  78 
  79 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
  80 
  81 /**
  82  * Default skin implementation for the {@link ToolBar} control.
  83  *
  84  * @see ToolBar
  85  * @since 9
  86  */
  87 public class ToolBarSkin extends SkinBase<ToolBar> {
  88 
  89     /***************************************************************************
  90      *                                                                         *
  91      * Private fields                                                          *
  92      *                                                                         *
  93      **************************************************************************/
  94 
  95     private Pane box;
  96     private ToolBarOverflowMenu overflowMenu;
  97     private boolean overflow = false;
  98     private double previousWidth = 0;
  99     private double previousHeight = 0;
 100     private double savedPrefWidth = 0;
 101     private double savedPrefHeight = 0;
 102     private ObservableList<MenuItem> overflowMenuItems;
 103     private boolean needsUpdate = false;
 104     private final ParentTraversalEngine engine;
 105     private final BehaviorBase<ToolBar> behavior;
 106 
 107 
 108 
 109     /***************************************************************************
 110      *                                                                         *
 111      * Constructors                                                            *
 112      *                                                                         *
 113      **************************************************************************/
 114 
 115     /**
 116      * Creates a new ToolBarSkin instance, installing the necessary child
 117      * nodes into the Control {@link Control#getChildren() children} list, as
 118      * well as the necessary input mappings for handling key, mouse, etc events.
 119      *
 120      * @param control The control that this skin should be installed onto.
 121      */
 122     public ToolBarSkin(ToolBar control) {
 123         super(control);
 124 
 125         // install default input map for the ToolBar control
 126         behavior = new ToolBarBehavior(control);
 127 //        control.setInputMap(behavior.getInputMap());
 128 
 129         overflowMenuItems = FXCollections.observableArrayList();
 130         initialize();
 131         registerChangeListener(control.orientationProperty(), e -> initialize());
 132 
 133         engine = new ParentTraversalEngine(getSkinnable(), new Algorithm() {
 134 
 135             private Node selectPrev(int from, TraversalContext context) {
 136                 for (int i = from; i >= 0; --i) {
 137                     Node n = box.getChildren().get(i);
 138                     if (n.isDisabled() || !n.impl_isTreeVisible()) continue;
 139                     if (n instanceof Parent) {
 140                         Node selected = context.selectLastInParent((Parent)n);
 141                         if (selected != null) return selected;
 142                     }
 143                     if (n.isFocusTraversable() ) {
 144                         return n;
 145                     }
 146                 }
 147                 return null;
 148             }
 149 
 150             private Node selectNext(int from, TraversalContext context) {
 151                 for (int i = from, max = box.getChildren().size(); i < max; ++i) {
 152                     Node n = box.getChildren().get(i);
 153                     if (n.isDisabled() || !n.impl_isTreeVisible()) continue;
 154                     if (n.isFocusTraversable()) {
 155                         return n;
 156                     }
 157                     if (n instanceof Parent) {
 158                         Node selected = context.selectFirstInParent((Parent)n);
 159                         if (selected != null) return selected;
 160                     }
 161                 }
 162                 return null;
 163             }
 164 
 165             @Override
 166             public Node select(Node owner, Direction dir, TraversalContext context) {
 167                 final ObservableList<Node> boxChildren = box.getChildren();
 168                 if (owner == overflowMenu) {
 169                     if (dir.isForward()) {
 170                         return null;
 171                     } else {
 172                         Node selected = selectPrev(boxChildren.size() - 1, context);
 173                         if (selected != null) return selected;
 174                     }
 175                 }
 176 
 177                 int idx = boxChildren.indexOf(owner);
 178 
 179                 if (idx < 0) {
 180                     // The current focus owner is a child of some Toolbar's item
 181                     Parent item = owner.getParent();
 182                     while (!boxChildren.contains(item)) {
 183                         item = item.getParent();
 184                     }
 185                     Node selected = context.selectInSubtree(item, owner, dir);
 186                     if (selected != null) return selected;
 187                     idx = boxChildren.indexOf(owner);
 188                     if (dir == Direction.NEXT) dir = Direction.NEXT_IN_LINE;
 189                 }
 190 
 191                 if (idx >= 0) {
 192                     if (dir.isForward()) {
 193                         Node selected = selectNext(idx + 1, context);
 194                         if (selected != null) return selected;
 195                         if (overflow) {
 196                             overflowMenu.requestFocus();
 197                             return overflowMenu;
 198                         }
 199                     } else {
 200                         Node selected = selectPrev(idx - 1, context);
 201                         if (selected != null) return selected;
 202                     }
 203                 }
 204                 return null;
 205             }
 206 
 207             @Override
 208             public Node selectFirst(TraversalContext context) {
 209                 Node selected = selectNext(0, context);
 210                 if (selected != null) return selected;
 211                 if (overflow) {
 212                     return overflowMenu;
 213                 }
 214                 return null;
 215             }
 216 
 217             @Override
 218             public Node selectLast(TraversalContext context) {
 219                 if (overflow) {
 220                     return overflowMenu;
 221                 }
 222                 return selectPrev(box.getChildren().size() - 1, context);
 223             }
 224         });
 225         ParentHelper.setTraversalEngine(getSkinnable(), engine);
 226 
 227         control.focusedProperty().addListener((observable, oldValue, newValue) -> {
 228             if (newValue) {
 229                 // TODO need to detect the focus direction
 230                 // to selected the first control in the toolbar when TAB is pressed
 231                 // or select the last control in the toolbar when SHIFT TAB is pressed.
 232                 if (!box.getChildren().isEmpty()) {
 233                     box.getChildren().get(0).requestFocus();
 234                 } else {
 235                     overflowMenu.requestFocus();
 236                 }
 237             }
 238         });
 239 
 240         control.getItems().addListener((ListChangeListener<Node>) c -> {
 241             while (c.next()) {
 242                 for (Node n: c.getRemoved()) {
 243                     box.getChildren().remove(n);
 244                 }
 245                 box.getChildren().addAll(c.getAddedSubList());
 246             }
 247             needsUpdate = true;
 248             getSkinnable().requestLayout();
 249         });
 250     }
 251 
 252 
 253 
 254     /***************************************************************************
 255      *                                                                         *
 256      * Properties                                                              *
 257      *                                                                         *
 258      **************************************************************************/
 259 
 260     // --- spacing
 261     private DoubleProperty spacing;
 262     private final void setSpacing(double value) {
 263         spacingProperty().set(snapSpace(value));
 264     }
 265 
 266     private final double getSpacing() {
 267         return spacing == null ? 0.0 : snapSpace(spacing.get());
 268     }
 269 
 270     private final DoubleProperty spacingProperty() {
 271         if (spacing == null) {
 272             spacing = new StyleableDoubleProperty() {
 273 
 274                 @Override
 275                 protected void invalidated() {
 276                     final double value = get();
 277                     if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 278                         ((VBox)box).setSpacing(value);
 279                     } else {
 280                         ((HBox)box).setSpacing(value);
 281                     }
 282                 }
 283 
 284                 @Override
 285                 public Object getBean() {
 286                     return ToolBarSkin.this;
 287                 }
 288 
 289                 @Override
 290                 public String getName() {
 291                     return "spacing";
 292                 }
 293 
 294                 @Override
 295                 public CssMetaData<ToolBar,Number> getCssMetaData() {
 296                     return StyleableProperties.SPACING;
 297                 }
 298             };
 299         }
 300         return spacing;
 301     }
 302 
 303     // --- box alignment
 304     private ObjectProperty<Pos> boxAlignment;
 305     private final void setBoxAlignment(Pos value) {
 306         boxAlignmentProperty().set(value);
 307     }
 308 
 309     private final Pos getBoxAlignment() {
 310         return boxAlignment == null ? Pos.TOP_LEFT : boxAlignment.get();
 311     }
 312 
 313     private final ObjectProperty<Pos> boxAlignmentProperty() {
 314         if (boxAlignment == null) {
 315             boxAlignment = new StyleableObjectProperty<Pos>(Pos.TOP_LEFT) {
 316 
 317                 @Override
 318                 public void invalidated() {
 319                     final Pos value = get();
 320                     if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 321                         ((VBox)box).setAlignment(value);
 322                     } else {
 323                         ((HBox)box).setAlignment(value);
 324                     }
 325                 }
 326 
 327                 @Override
 328                 public Object getBean() {
 329                     return ToolBarSkin.this;
 330                 }
 331 
 332                 @Override
 333                 public String getName() {
 334                     return "boxAlignment";
 335                 }
 336 
 337                 @Override
 338                 public CssMetaData<ToolBar,Pos> getCssMetaData() {
 339                     return StyleableProperties.ALIGNMENT;
 340                 }
 341             };
 342         }
 343         return boxAlignment;
 344     }
 345 
 346 
 347 
 348     /***************************************************************************
 349      *                                                                         *
 350      * Public API                                                              *
 351      *                                                                         *
 352      **************************************************************************/
 353 
 354     /** {@inheritDoc} */
 355     @Override public void dispose() {
 356         super.dispose();
 357 
 358         if (behavior != null) {
 359             behavior.dispose();
 360         }
 361     }
 362 
 363     /** {@inheritDoc} */
 364     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 365         final ToolBar toolbar = getSkinnable();
 366         return toolbar.getOrientation() == Orientation.VERTICAL ?
 367             computePrefWidth(-1, topInset, rightInset, bottomInset, leftInset) :
 368             snapSize(overflowMenu.prefWidth(-1)) + leftInset + rightInset;
 369     }
 370 
 371     /** {@inheritDoc} */
 372     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 373         final ToolBar toolbar = getSkinnable();
 374         return toolbar.getOrientation() == Orientation.VERTICAL?
 375             snapSize(overflowMenu.prefHeight(-1)) + topInset + bottomInset :
 376             computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset);
 377     }
 378 
 379     /** {@inheritDoc} */
 380     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 381         double prefWidth = 0;
 382         final ToolBar toolbar = getSkinnable();
 383 
 384         if (toolbar.getOrientation() == Orientation.HORIZONTAL) {
 385             for (Node node : toolbar.getItems()) {
 386                 if (!node.isManaged()) continue;
 387                 prefWidth += snapSize(node.prefWidth(-1)) + getSpacing();
 388             }
 389             prefWidth -= getSpacing();
 390         } else {
 391             for (Node node : toolbar.getItems()) {
 392                 if (!node.isManaged()) continue;
 393                 prefWidth = Math.max(prefWidth, snapSize(node.prefWidth(-1)));
 394             }
 395             if (toolbar.getItems().size() > 0) {
 396                 savedPrefWidth = prefWidth;
 397             } else {
 398                 prefWidth = savedPrefWidth;
 399             }
 400         }
 401         return leftInset + prefWidth + rightInset;
 402     }
 403 
 404     /** {@inheritDoc} */
 405     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 406         double prefHeight = 0;
 407         final ToolBar toolbar = getSkinnable();
 408 
 409         if(toolbar.getOrientation() == Orientation.VERTICAL) {
 410             for (Node node: toolbar.getItems()) {
 411                 if (!node.isManaged()) continue;
 412                 prefHeight += snapSize(node.prefHeight(-1)) + getSpacing();
 413             }
 414             prefHeight -= getSpacing();
 415         } else {
 416             for (Node node : toolbar.getItems()) {
 417                 if (!node.isManaged()) continue;
 418                 prefHeight = Math.max(prefHeight, snapSize(node.prefHeight(-1)));
 419             }
 420             if (toolbar.getItems().size() > 0) {
 421                 savedPrefHeight = prefHeight;
 422             } else {
 423                 prefHeight = savedPrefHeight;
 424             }
 425         }
 426         return topInset + prefHeight + bottomInset;
 427     }
 428 
 429     /** {@inheritDoc} */
 430     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 431         return getSkinnable().getOrientation() == Orientation.VERTICAL ?
 432                 snapSize(getSkinnable().prefWidth(-1)) : Double.MAX_VALUE;
 433     }
 434 
 435     /** {@inheritDoc} */
 436     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 437         return getSkinnable().getOrientation() == Orientation.VERTICAL ?
 438                 Double.MAX_VALUE : snapSize(getSkinnable().prefHeight(-1));
 439     }
 440 
 441     /** {@inheritDoc} */
 442     @Override protected void layoutChildren(final double x,final double y,
 443             final double w, final double h) {
 444 //        super.layoutChildren();
 445         final ToolBar toolbar = getSkinnable();
 446 
 447         if (toolbar.getOrientation() == Orientation.VERTICAL) {
 448             if (snapSize(toolbar.getHeight()) != previousHeight || needsUpdate) {
 449                 ((VBox)box).setSpacing(getSpacing());
 450                 ((VBox)box).setAlignment(getBoxAlignment());
 451                 previousHeight = snapSize(toolbar.getHeight());
 452                 addNodesToToolBar();
 453             }
 454         } else {
 455             if (snapSize(toolbar.getWidth()) != previousWidth || needsUpdate) {
 456                 ((HBox)box).setSpacing(getSpacing());
 457                 ((HBox)box).setAlignment(getBoxAlignment());
 458                 previousWidth = snapSize(toolbar.getWidth());
 459                 addNodesToToolBar();
 460             }
 461         }
 462         needsUpdate = false;
 463 
 464         double toolbarWidth = w;
 465         double toolbarHeight = h;
 466 
 467         if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 468             toolbarHeight -= (overflow ? snapSize(overflowMenu.prefHeight(-1)) : 0);
 469         } else {
 470             toolbarWidth -= (overflow ? snapSize(overflowMenu.prefWidth(-1)) : 0);
 471         }
 472 
 473         box.resize(toolbarWidth, toolbarHeight);
 474         positionInArea(box, x, y,
 475                 toolbarWidth, toolbarHeight, /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
 476 
 477         // If popup menu is not null show the overflowControl
 478         if (overflow) {
 479             double overflowMenuWidth = snapSize(overflowMenu.prefWidth(-1));
 480             double overflowMenuHeight = snapSize(overflowMenu.prefHeight(-1));
 481             double overflowX = x;
 482             double overflowY = x;
 483             if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 484                 // This is to prevent the overflow menu from moving when there
 485                 // are no items in the toolbar.
 486                 if (toolbarWidth == 0) {
 487                     toolbarWidth = savedPrefWidth;
 488                 }
 489                 HPos pos = ((VBox)box).getAlignment().getHpos();
 490                 if (HPos.LEFT.equals(pos)) {
 491                     overflowX = x + Math.abs((toolbarWidth - overflowMenuWidth)/2);
 492                 } else if (HPos.RIGHT.equals(pos)) {
 493                     overflowX = (snapSize(toolbar.getWidth()) - snappedRightInset() - toolbarWidth) +
 494                         Math.abs((toolbarWidth - overflowMenuWidth)/2);
 495                 } else {
 496                     overflowX = x +
 497                         Math.abs((snapSize(toolbar.getWidth()) - (x) +
 498                         snappedRightInset() - overflowMenuWidth)/2);
 499                 }
 500                 overflowY = snapSize(toolbar.getHeight()) - overflowMenuHeight - y;
 501             } else {
 502                 // This is to prevent the overflow menu from moving when there
 503                 // are no items in the toolbar.
 504                 if (toolbarHeight == 0) {
 505                     toolbarHeight = savedPrefHeight;
 506                 }
 507                 VPos pos = ((HBox)box).getAlignment().getVpos();
 508                 if (VPos.TOP.equals(pos)) {
 509                     overflowY = y +
 510                         Math.abs((toolbarHeight - overflowMenuHeight)/2);
 511                 } else if (VPos.BOTTOM.equals(pos)) {
 512                     overflowY = (snapSize(toolbar.getHeight()) - snappedBottomInset() - toolbarHeight) +
 513                         Math.abs((toolbarHeight - overflowMenuHeight)/2);
 514                 } else {
 515                     overflowY = y + Math.abs((toolbarHeight - overflowMenuHeight)/2);
 516                 }
 517                overflowX = snapSize(toolbar.getWidth()) - overflowMenuWidth - snappedRightInset();
 518             }
 519             overflowMenu.resize(overflowMenuWidth, overflowMenuHeight);
 520             positionInArea(overflowMenu, overflowX, overflowY, overflowMenuWidth, overflowMenuHeight, /*baseline ignored*/0,
 521                     HPos.CENTER, VPos.CENTER);
 522         }
 523     }
 524 
 525 
 526 
 527     /***************************************************************************
 528      *                                                                         *
 529      * Private implementation                                                  *
 530      *                                                                         *
 531      **************************************************************************/
 532 
 533     private void initialize() {
 534         if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 535             box = new VBox();
 536         } else {
 537             box = new HBox();
 538         }
 539         box.getStyleClass().add("container");
 540         box.getChildren().addAll(getSkinnable().getItems());
 541         overflowMenu = new ToolBarOverflowMenu(overflowMenuItems);
 542         overflowMenu.setVisible(false);
 543         overflowMenu.setManaged(false);
 544 
 545         getChildren().clear();
 546         getChildren().add(box);
 547         getChildren().add(overflowMenu);
 548 
 549         previousWidth = 0;
 550         previousHeight = 0;
 551         savedPrefWidth = 0;
 552         savedPrefHeight = 0;
 553         needsUpdate = true;
 554         getSkinnable().requestLayout();
 555     }
 556 
 557     private void addNodesToToolBar() {
 558         final ToolBar toolbar = getSkinnable();
 559         double length = 0;
 560         if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 561             length = snapSize(toolbar.getHeight()) - snappedTopInset() - snappedBottomInset() + getSpacing();
 562         } else {
 563             length = snapSize(toolbar.getWidth()) - snappedLeftInset() - snappedRightInset() + getSpacing();
 564         }
 565 
 566         // Is there overflow ?
 567         double x = 0;
 568         boolean hasOverflow = false;
 569         for (Node node : getSkinnable().getItems()) {
 570             if (!node.isManaged()) continue;
 571 
 572             if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 573                 x += snapSize(node.prefHeight(-1)) + getSpacing();
 574             } else {
 575                 x += snapSize(node.prefWidth(-1)) + getSpacing();
 576             }
 577             if (x > length) {
 578                 hasOverflow = true;
 579                 break;
 580             }
 581         }
 582 
 583         if (hasOverflow) {
 584             if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 585                 length -= snapSize(overflowMenu.prefHeight(-1));
 586             } else {
 587                 length -= snapSize(overflowMenu.prefWidth(-1));
 588             }
 589             length -= getSpacing();
 590         }
 591 
 592         // Determine which node goes to the toolbar and which goes to the overflow.
 593         x = 0;
 594         overflowMenuItems.clear();
 595         box.getChildren().clear();
 596         for (Node node : getSkinnable().getItems()) {
 597             node.getStyleClass().remove("menu-item");
 598             node.getStyleClass().remove("custom-menu-item");
 599 
 600             if (node.isManaged()) {
 601                 if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 602                     x += snapSize(node.prefHeight(-1)) + getSpacing();
 603                 } else {
 604                     x += snapSize(node.prefWidth(-1)) + getSpacing();
 605                 }
 606             }
 607 
 608             if (x <= length) {
 609                 box.getChildren().add(node);
 610             } else {
 611                 if (node.isFocused()) {
 612                     if (!box.getChildren().isEmpty()) {
 613                         Node last = engine.selectLast();
 614                         if (last != null) {
 615                             last.requestFocus();
 616                         }
 617                     } else {
 618                         overflowMenu.requestFocus();
 619                     }
 620                 }
 621                 if (node instanceof Separator) {
 622                     overflowMenuItems.add(new SeparatorMenuItem());
 623                 } else {
 624                     CustomMenuItem customMenuItem = new CustomMenuItem(node);
 625 
 626                     // RT-36455:
 627                     // We can't be totally certain of all nodes, but for the
 628                     // most common nodes we can check to see whether we should
 629                     // hide the menu when the node is clicked on. The common
 630                     // case is for TextField or Slider.
 631                     // This list won't be exhaustive (there is no point really
 632                     // considering the ListView case), but it should try to
 633                     // include most common control types that find themselves
 634                     // placed in menus.
 635                     final String nodeType = node.getTypeSelector();
 636                     switch (nodeType) {
 637                         case "Button":
 638                         case "Hyperlink":
 639                         case "Label":
 640                             customMenuItem.setHideOnClick(true);
 641                             break;
 642                         case "CheckBox":
 643                         case "ChoiceBox":
 644                         case "ColorPicker":
 645                         case "ComboBox":
 646                         case "DatePicker":
 647                         case "MenuButton":
 648                         case "PasswordField":
 649                         case "RadioButton":
 650                         case "ScrollBar":
 651                         case "ScrollPane":
 652                         case "Slider":
 653                         case "SplitMenuButton":
 654                         case "SplitPane":
 655                         case "TextArea":
 656                         case "TextField":
 657                         case "ToggleButton":
 658                         case "ToolBar":
 659                         default:
 660                             customMenuItem.setHideOnClick(false);
 661                             break;
 662                     }
 663 
 664                     overflowMenuItems.add(customMenuItem);
 665                 }
 666             }
 667         }
 668 
 669         // Check if we overflowed.
 670         overflow = overflowMenuItems.size() > 0;
 671         if (!overflow && overflowMenu.isFocused()) {
 672             Node last = engine.selectLast();
 673             if (last != null) {
 674                 last.requestFocus();
 675             }
 676         }
 677         overflowMenu.setVisible(overflow);
 678         overflowMenu.setManaged(overflow);
 679     }
 680 
 681 
 682 
 683     /***************************************************************************
 684      *                                                                         *
 685      * Support classes                                                         *
 686      *                                                                         *
 687      **************************************************************************/
 688 
 689     class ToolBarOverflowMenu extends StackPane {
 690         private StackPane downArrow;
 691         private ContextMenu popup;
 692         private ObservableList<MenuItem> menuItems;
 693 
 694         public ToolBarOverflowMenu(ObservableList<MenuItem> items) {
 695             getStyleClass().setAll("tool-bar-overflow-button");
 696             setAccessibleRole(AccessibleRole.BUTTON);
 697             setAccessibleText(getString("Accessibility.title.ToolBar.OverflowButton"));
 698             setFocusTraversable(true);
 699             this.menuItems = items;
 700             downArrow = new StackPane();
 701             downArrow.getStyleClass().setAll("arrow");
 702             downArrow.setOnMousePressed(me -> {
 703                 fire();
 704             });
 705 
 706             setOnKeyPressed(ke -> {
 707                 if (KeyCode.SPACE.equals(ke.getCode())) {
 708                     if (!popup.isShowing()) {
 709                         popup.getItems().clear();
 710                         popup.getItems().addAll(menuItems);
 711                         popup.show(downArrow, Side.BOTTOM, 0, 0);
 712                     }
 713                     ke.consume();
 714                 } else if (KeyCode.ESCAPE.equals(ke.getCode())) {
 715                     if (popup.isShowing()) {
 716                         popup.hide();
 717                     }
 718                     ke.consume();
 719                 } else if (KeyCode.ENTER.equals(ke.getCode())) {
 720                     fire();
 721                     ke.consume();
 722                 }
 723             });
 724 
 725             visibleProperty().addListener((observable, oldValue, newValue) -> {
 726                     if (newValue) {
 727                         if (box.getChildren().isEmpty()) {
 728                             setFocusTraversable(true);
 729                         }
 730                     }
 731             });
 732             popup = new ContextMenu();
 733             setVisible(false);
 734             setManaged(false);
 735             getChildren().add(downArrow);
 736         }
 737 
 738         private void fire() {
 739             if (popup.isShowing()) {
 740                 popup.hide();
 741             } else {
 742                 popup.getItems().clear();
 743                 popup.getItems().addAll(menuItems);
 744                 popup.show(downArrow, Side.BOTTOM, 0, 0);
 745             }
 746         }
 747 
 748         @Override protected double computePrefWidth(double height) {
 749             return snappedLeftInset() + snappedRightInset();
 750         }
 751 
 752         @Override protected double computePrefHeight(double width) {
 753             return snappedTopInset() + snappedBottomInset();
 754         }
 755 
 756         @Override protected void layoutChildren() {
 757             double w = snapSize(downArrow.prefWidth(-1));
 758             double h = snapSize(downArrow.prefHeight(-1));
 759             double x = (snapSize(getWidth()) - w)/2;
 760             double y = (snapSize(getHeight()) - h)/2;
 761 
 762             // TODO need to provide support for when the toolbar is on the right
 763             // or bottom
 764             if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 765                 downArrow.setRotate(0);
 766             }
 767 
 768             downArrow.resize(w, h);
 769             positionInArea(downArrow, x, y, w, h,
 770                     /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
 771         }
 772 
 773         @Override
 774         public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 775             switch (action) {
 776                 case FIRE: fire(); break;
 777                 default: super.executeAccessibleAction(action); break;
 778             }
 779         }
 780     }
 781 
 782     /***************************************************************************
 783      *                                                                         *
 784      *                         Stylesheet Handling                             *
 785      *                                                                         *
 786      **************************************************************************/
 787 
 788      /*
 789       * Super-lazy instantiation pattern from Bill Pugh.
 790       */
 791      private static class StyleableProperties {
 792          private static final CssMetaData<ToolBar,Number> SPACING =
 793              new CssMetaData<ToolBar,Number>("-fx-spacing",
 794                  SizeConverter.getInstance(), 0.0) {
 795 
 796             @Override
 797             public boolean isSettable(ToolBar n) {
 798                 final ToolBarSkin skin = (ToolBarSkin) n.getSkin();
 799                 return skin.spacing == null || !skin.spacing.isBound();
 800             }
 801 
 802             @Override
 803             public StyleableProperty<Number> getStyleableProperty(ToolBar n) {
 804                 final ToolBarSkin skin = (ToolBarSkin) n.getSkin();
 805                 return (StyleableProperty<Number>)(WritableValue<Number>)skin.spacingProperty();
 806             }
 807         };
 808 
 809         private static final CssMetaData<ToolBar,Pos>ALIGNMENT =
 810                 new CssMetaData<ToolBar,Pos>("-fx-alignment",
 811                 new EnumConverter<Pos>(Pos.class), Pos.TOP_LEFT ) {
 812 
 813             @Override
 814             public boolean isSettable(ToolBar n) {
 815                 final ToolBarSkin skin = (ToolBarSkin) n.getSkin();
 816                 return skin.boxAlignment == null || !skin.boxAlignment.isBound();
 817             }
 818 
 819             @Override
 820             public StyleableProperty<Pos> getStyleableProperty(ToolBar n) {
 821                 final ToolBarSkin skin = (ToolBarSkin) n.getSkin();
 822                 return (StyleableProperty<Pos>)(WritableValue<Pos>)skin.boxAlignmentProperty();
 823             }
 824         };
 825 
 826 
 827          private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 828          static {
 829 
 830             final List<CssMetaData<? extends Styleable, ?>> styleables =
 831                 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
 832 
 833             // StackPane also has -fx-alignment. Replace it with
 834             // ToolBarSkin's.
 835             // TODO: Really should be able to reference StackPane.StyleableProperties.ALIGNMENT
 836             final String alignmentProperty = ALIGNMENT.getProperty();
 837             for (int n=0, nMax=styleables.size(); n<nMax; n++) {
 838                 final CssMetaData<?,?> prop = styleables.get(n);
 839                 if (alignmentProperty.equals(prop.getProperty())) styleables.remove(prop);
 840             }
 841 
 842             styleables.add(SPACING);
 843             styleables.add(ALIGNMENT);
 844             STYLEABLES = Collections.unmodifiableList(styleables);
 845 
 846          }
 847     }
 848 
 849     /**
 850      * Returns the CssMetaData associated with this class, which may include the
 851      * CssMetaData of its super classes.
 852      */
 853     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 854         return StyleableProperties.STYLEABLES;
 855     }
 856 
 857     /**
 858      * {@inheritDoc}
 859      */
 860     @Override
 861     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 862         return getClassCssMetaData();
 863     }
 864 
 865     @Override
 866     protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 867         switch (attribute) {
 868             case OVERFLOW_BUTTON: return overflowMenu;
 869             default: return super.queryAccessibleAttribute(attribute, parameters);
 870         }
 871     }
 872 
 873     @Override
 874     protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 875         switch (action) {
 876             case SHOW_MENU:
 877                 overflowMenu.fire();
 878                 break;
 879             default: super.executeAccessibleAction(action, parameters);
 880         }
 881     }
 882 }