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