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