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