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