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