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