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 private double snapSpacing(double value) { 262 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 263 return snapSpaceY(value); 264 } else { 265 return snapSpaceX(value); 266 } 267 } 268 269 // --- spacing 270 private DoubleProperty spacing; 271 private final void setSpacing(double value) { 272 spacingProperty().set(snapSpacing(value)); 273 } 274 275 private final double getSpacing() { 276 return spacing == null ? 0.0 : snapSpacing(spacing.get()); 277 } 278 279 private final DoubleProperty spacingProperty() { 280 if (spacing == null) { 281 spacing = new StyleableDoubleProperty() { 282 283 @Override 284 protected void invalidated() { 285 final double value = get(); 286 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 287 ((VBox)box).setSpacing(value); 288 } else { 289 ((HBox)box).setSpacing(value); 290 } 291 } 292 293 @Override 294 public Object getBean() { 295 return ToolBarSkin.this; 296 } 297 298 @Override 299 public String getName() { 300 return "spacing"; 301 } 302 303 @Override 304 public CssMetaData<ToolBar,Number> getCssMetaData() { 305 return StyleableProperties.SPACING; 306 } 307 }; 308 } 309 return spacing; 310 } 311 312 // --- box alignment 313 private ObjectProperty<Pos> boxAlignment; 314 private final void setBoxAlignment(Pos value) { 315 boxAlignmentProperty().set(value); 316 } 317 318 private final Pos getBoxAlignment() { 319 return boxAlignment == null ? Pos.TOP_LEFT : boxAlignment.get(); 320 } 321 322 private final ObjectProperty<Pos> boxAlignmentProperty() { 323 if (boxAlignment == null) { 324 boxAlignment = new StyleableObjectProperty<Pos>(Pos.TOP_LEFT) { 325 326 @Override 327 public void invalidated() { 328 final Pos value = get(); 329 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 330 ((VBox)box).setAlignment(value); 331 } else { 332 ((HBox)box).setAlignment(value); 333 } 334 } 335 336 @Override 337 public Object getBean() { 338 return ToolBarSkin.this; 339 } 340 341 @Override 342 public String getName() { 343 return "boxAlignment"; 344 } 345 346 @Override 347 public CssMetaData<ToolBar,Pos> getCssMetaData() { 348 return StyleableProperties.ALIGNMENT; 349 } 350 }; 351 } 352 return boxAlignment; 353 } 354 355 356 357 /*************************************************************************** 358 * * 359 * Public API * 360 * * 361 **************************************************************************/ 362 363 /** {@inheritDoc} */ 364 @Override public void dispose() { 365 super.dispose(); 366 367 if (behavior != null) { 368 behavior.dispose(); 369 } 370 } 371 372 /** {@inheritDoc} */ 373 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 374 final ToolBar toolbar = getSkinnable(); 375 return toolbar.getOrientation() == Orientation.VERTICAL ? 376 computePrefWidth(-1, topInset, rightInset, bottomInset, leftInset) : 377 snapSizeX(overflowMenu.prefWidth(-1)) + leftInset + rightInset; 378 } 379 380 /** {@inheritDoc} */ 381 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 382 final ToolBar toolbar = getSkinnable(); 383 return toolbar.getOrientation() == Orientation.VERTICAL? 384 snapSizeY(overflowMenu.prefHeight(-1)) + topInset + bottomInset : 385 computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset); 386 } 387 388 /** {@inheritDoc} */ 389 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 390 double prefWidth = 0; 391 final ToolBar toolbar = getSkinnable(); 392 393 if (toolbar.getOrientation() == Orientation.HORIZONTAL) { 394 for (Node node : toolbar.getItems()) { 395 if (!node.isManaged()) continue; 396 prefWidth += snapSizeX(node.prefWidth(-1)) + getSpacing(); 397 } 398 prefWidth -= getSpacing(); 399 } else { 400 for (Node node : toolbar.getItems()) { 401 if (!node.isManaged()) continue; 402 prefWidth = Math.max(prefWidth, snapSizeX(node.prefWidth(-1))); 403 } 404 if (toolbar.getItems().size() > 0) { 405 savedPrefWidth = prefWidth; 406 } else { 407 prefWidth = savedPrefWidth; 408 } 409 } 410 return leftInset + prefWidth + rightInset; 411 } 412 413 /** {@inheritDoc} */ 414 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 415 double prefHeight = 0; 416 final ToolBar toolbar = getSkinnable(); 417 418 if(toolbar.getOrientation() == Orientation.VERTICAL) { 419 for (Node node: toolbar.getItems()) { 420 if (!node.isManaged()) continue; 421 prefHeight += snapSizeY(node.prefHeight(-1)) + getSpacing(); 422 } 423 prefHeight -= getSpacing(); 424 } else { 425 for (Node node : toolbar.getItems()) { 426 if (!node.isManaged()) continue; 427 prefHeight = Math.max(prefHeight, snapSizeY(node.prefHeight(-1))); 428 } 429 if (toolbar.getItems().size() > 0) { 430 savedPrefHeight = prefHeight; 431 } else { 432 prefHeight = savedPrefHeight; 433 } 434 } 435 return topInset + prefHeight + bottomInset; 436 } 437 438 /** {@inheritDoc} */ 439 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 440 return getSkinnable().getOrientation() == Orientation.VERTICAL ? 441 snapSizeX(getSkinnable().prefWidth(-1)) : Double.MAX_VALUE; 442 } 443 444 /** {@inheritDoc} */ 445 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 446 return getSkinnable().getOrientation() == Orientation.VERTICAL ? 447 Double.MAX_VALUE : snapSizeY(getSkinnable().prefHeight(-1)); 448 } 449 450 /** {@inheritDoc} */ 451 @Override protected void layoutChildren(final double x,final double y, 452 final double w, final double h) { 453 // super.layoutChildren(); 454 final ToolBar toolbar = getSkinnable(); 455 456 if (toolbar.getOrientation() == Orientation.VERTICAL) { 457 if (snapSizeY(toolbar.getHeight()) != previousHeight || needsUpdate) { 458 ((VBox)box).setSpacing(getSpacing()); 459 ((VBox)box).setAlignment(getBoxAlignment()); 460 previousHeight = snapSizeY(toolbar.getHeight()); 461 addNodesToToolBar(); 462 } 463 } else { 464 if (snapSizeX(toolbar.getWidth()) != previousWidth || needsUpdate) { 465 ((HBox)box).setSpacing(getSpacing()); 466 ((HBox)box).setAlignment(getBoxAlignment()); 467 previousWidth = snapSizeX(toolbar.getWidth()); 468 addNodesToToolBar(); 469 } 470 } 471 needsUpdate = false; 472 473 double toolbarWidth = w; 474 double toolbarHeight = h; 475 476 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 477 toolbarHeight -= (overflow ? snapSizeY(overflowMenu.prefHeight(-1)) : 0); 478 } else { 479 toolbarWidth -= (overflow ? snapSizeX(overflowMenu.prefWidth(-1)) : 0); 480 } 481 482 box.resize(toolbarWidth, toolbarHeight); 483 positionInArea(box, x, y, 484 toolbarWidth, toolbarHeight, /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 485 486 // If popup menu is not null show the overflowControl 487 if (overflow) { 488 double overflowMenuWidth = snapSizeX(overflowMenu.prefWidth(-1)); 489 double overflowMenuHeight = snapSizeY(overflowMenu.prefHeight(-1)); 490 double overflowX = x; 491 double overflowY = x; 492 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 493 // This is to prevent the overflow menu from moving when there 494 // are no items in the toolbar. 495 if (toolbarWidth == 0) { 496 toolbarWidth = savedPrefWidth; 497 } 498 HPos pos = ((VBox)box).getAlignment().getHpos(); 499 if (HPos.LEFT.equals(pos)) { 500 overflowX = x + Math.abs((toolbarWidth - overflowMenuWidth)/2); 501 } else if (HPos.RIGHT.equals(pos)) { 502 overflowX = (snapSizeX(toolbar.getWidth()) - snappedRightInset() - toolbarWidth) + 503 Math.abs((toolbarWidth - overflowMenuWidth)/2); 504 } else { 505 overflowX = x + 506 Math.abs((snapSizeX(toolbar.getWidth()) - (x) + 507 snappedRightInset() - overflowMenuWidth)/2); 508 } 509 overflowY = snapSizeY(toolbar.getHeight()) - overflowMenuHeight - y; 510 } else { 511 // This is to prevent the overflow menu from moving when there 512 // are no items in the toolbar. 513 if (toolbarHeight == 0) { 514 toolbarHeight = savedPrefHeight; 515 } 516 VPos pos = ((HBox)box).getAlignment().getVpos(); 517 if (VPos.TOP.equals(pos)) { 518 overflowY = y + 519 Math.abs((toolbarHeight - overflowMenuHeight)/2); 520 } else if (VPos.BOTTOM.equals(pos)) { 521 overflowY = (snapSizeY(toolbar.getHeight()) - snappedBottomInset() - toolbarHeight) + 522 Math.abs((toolbarHeight - overflowMenuHeight)/2); 523 } else { 524 overflowY = y + Math.abs((toolbarHeight - overflowMenuHeight)/2); 525 } 526 overflowX = snapSizeX(toolbar.getWidth()) - overflowMenuWidth - snappedRightInset(); 527 } 528 overflowMenu.resize(overflowMenuWidth, overflowMenuHeight); 529 positionInArea(overflowMenu, overflowX, overflowY, overflowMenuWidth, overflowMenuHeight, /*baseline ignored*/0, 530 HPos.CENTER, VPos.CENTER); 531 } 532 } 533 534 535 536 /*************************************************************************** 537 * * 538 * Private implementation * 539 * * 540 **************************************************************************/ 541 542 private void initialize() { 543 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 544 box = new VBox(); 545 } else { 546 box = new HBox(); 547 } 548 box.getStyleClass().add("container"); 549 box.getChildren().addAll(getSkinnable().getItems()); 550 overflowMenu = new ToolBarOverflowMenu(overflowMenuItems); 551 overflowMenu.setVisible(false); 552 overflowMenu.setManaged(false); 553 554 getChildren().clear(); 555 getChildren().add(box); 556 getChildren().add(overflowMenu); 557 558 previousWidth = 0; 559 previousHeight = 0; 560 savedPrefWidth = 0; 561 savedPrefHeight = 0; 562 needsUpdate = true; 563 getSkinnable().requestLayout(); 564 } 565 566 private void addNodesToToolBar() { 567 final ToolBar toolbar = getSkinnable(); 568 double length = 0; 569 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 570 length = snapSizeY(toolbar.getHeight()) - snappedTopInset() - snappedBottomInset() + getSpacing(); 571 } else { 572 length = snapSizeX(toolbar.getWidth()) - snappedLeftInset() - snappedRightInset() + getSpacing(); 573 } 574 575 // Is there overflow ? 576 double x = 0; 577 boolean hasOverflow = false; 578 for (Node node : getSkinnable().getItems()) { 579 if (!node.isManaged()) continue; 580 581 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 582 x += snapSizeY(node.prefHeight(-1)) + getSpacing(); 583 } else { 584 x += snapSizeX(node.prefWidth(-1)) + getSpacing(); 585 } 586 if (x > length) { 587 hasOverflow = true; 588 break; 589 } 590 } 591 592 if (hasOverflow) { 593 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 594 length -= snapSizeY(overflowMenu.prefHeight(-1)); 595 } else { 596 length -= snapSizeX(overflowMenu.prefWidth(-1)); 597 } 598 length -= getSpacing(); 599 } 600 601 // Determine which node goes to the toolbar and which goes to the overflow. 602 x = 0; 603 overflowMenuItems.clear(); 604 box.getChildren().clear(); 605 for (Node node : getSkinnable().getItems()) { 606 node.getStyleClass().remove("menu-item"); 607 node.getStyleClass().remove("custom-menu-item"); 608 609 if (node.isManaged()) { 610 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 611 x += snapSizeY(node.prefHeight(-1)) + getSpacing(); 612 } else { 613 x += snapSizeX(node.prefWidth(-1)) + getSpacing(); 614 } 615 } 616 617 if (x <= length) { 618 box.getChildren().add(node); 619 } else { 620 if (node.isFocused()) { 621 if (!box.getChildren().isEmpty()) { 622 Node last = engine.selectLast(); 623 if (last != null) { 624 last.requestFocus(); 625 } 626 } else { 627 overflowMenu.requestFocus(); 628 } 629 } 630 if (node instanceof Separator) { 631 overflowMenuItems.add(new SeparatorMenuItem()); 632 } else { 633 CustomMenuItem customMenuItem = new CustomMenuItem(node); 634 635 // RT-36455: 636 // We can't be totally certain of all nodes, but for the 637 // most common nodes we can check to see whether we should 638 // hide the menu when the node is clicked on. The common 639 // case is for TextField or Slider. 640 // This list won't be exhaustive (there is no point really 641 // considering the ListView case), but it should try to 642 // include most common control types that find themselves 643 // placed in menus. 644 final String nodeType = node.getTypeSelector(); 645 switch (nodeType) { 646 case "Button": 647 case "Hyperlink": 648 case "Label": 649 customMenuItem.setHideOnClick(true); 650 break; 651 case "CheckBox": 652 case "ChoiceBox": 653 case "ColorPicker": 654 case "ComboBox": 655 case "DatePicker": 656 case "MenuButton": 657 case "PasswordField": 658 case "RadioButton": 659 case "ScrollBar": 660 case "ScrollPane": 661 case "Slider": 662 case "SplitMenuButton": 663 case "SplitPane": 664 case "TextArea": 665 case "TextField": 666 case "ToggleButton": 667 case "ToolBar": 668 default: 669 customMenuItem.setHideOnClick(false); 670 break; 671 } 672 673 overflowMenuItems.add(customMenuItem); 674 } 675 } 676 } 677 678 // Check if we overflowed. 679 overflow = overflowMenuItems.size() > 0; 680 if (!overflow && overflowMenu.isFocused()) { 681 Node last = engine.selectLast(); 682 if (last != null) { 683 last.requestFocus(); 684 } 685 } 686 overflowMenu.setVisible(overflow); 687 overflowMenu.setManaged(overflow); 688 } 689 690 691 692 /*************************************************************************** 693 * * 694 * Support classes * 695 * * 696 **************************************************************************/ 697 698 class ToolBarOverflowMenu extends StackPane { 699 private StackPane downArrow; 700 private ContextMenu popup; 701 private ObservableList<MenuItem> menuItems; 702 703 public ToolBarOverflowMenu(ObservableList<MenuItem> items) { 704 getStyleClass().setAll("tool-bar-overflow-button"); 705 setAccessibleRole(AccessibleRole.BUTTON); 706 setAccessibleText(getString("Accessibility.title.ToolBar.OverflowButton")); 707 setFocusTraversable(true); 708 this.menuItems = items; 709 downArrow = new StackPane(); 710 downArrow.getStyleClass().setAll("arrow"); 711 downArrow.setOnMousePressed(me -> { 712 fire(); 713 }); 714 715 setOnKeyPressed(ke -> { 716 if (KeyCode.SPACE.equals(ke.getCode())) { 717 if (!popup.isShowing()) { 718 popup.getItems().clear(); 719 popup.getItems().addAll(menuItems); 720 popup.show(downArrow, Side.BOTTOM, 0, 0); 721 } 722 ke.consume(); 723 } else if (KeyCode.ESCAPE.equals(ke.getCode())) { 724 if (popup.isShowing()) { 725 popup.hide(); 726 } 727 ke.consume(); 728 } else if (KeyCode.ENTER.equals(ke.getCode())) { 729 fire(); 730 ke.consume(); 731 } 732 }); 733 734 visibleProperty().addListener((observable, oldValue, newValue) -> { 735 if (newValue) { 736 if (box.getChildren().isEmpty()) { 737 setFocusTraversable(true); 738 } 739 } 740 }); 741 popup = new ContextMenu(); 742 setVisible(false); 743 setManaged(false); 744 getChildren().add(downArrow); 745 } 746 747 private void fire() { 748 if (popup.isShowing()) { 749 popup.hide(); 750 } else { 751 popup.getItems().clear(); 752 popup.getItems().addAll(menuItems); 753 popup.show(downArrow, Side.BOTTOM, 0, 0); 754 } 755 } 756 757 @Override protected double computePrefWidth(double height) { 758 return snappedLeftInset() + snappedRightInset(); 759 } 760 761 @Override protected double computePrefHeight(double width) { 762 return snappedTopInset() + snappedBottomInset(); 763 } 764 765 @Override protected void layoutChildren() { 766 double w = snapSize(downArrow.prefWidth(-1)); 767 double h = snapSize(downArrow.prefHeight(-1)); 768 double x = (snapSize(getWidth()) - w)/2; 769 double y = (snapSize(getHeight()) - h)/2; 770 771 // TODO need to provide support for when the toolbar is on the right 772 // or bottom 773 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 774 downArrow.setRotate(0); 775 } 776 777 downArrow.resize(w, h); 778 positionInArea(downArrow, x, y, w, h, 779 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 780 } 781 782 @Override 783 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 784 switch (action) { 785 case FIRE: fire(); break; 786 default: super.executeAccessibleAction(action); break; 787 } 788 } 789 } 790 791 /*************************************************************************** 792 * * 793 * Stylesheet Handling * 794 * * 795 **************************************************************************/ 796 797 /* 798 * Super-lazy instantiation pattern from Bill Pugh. 799 */ 800 private static class StyleableProperties { 801 private static final CssMetaData<ToolBar,Number> SPACING = 802 new CssMetaData<ToolBar,Number>("-fx-spacing", 803 SizeConverter.getInstance(), 0.0) { 804 805 @Override 806 public boolean isSettable(ToolBar n) { 807 final ToolBarSkin skin = (ToolBarSkin) n.getSkin(); 808 return skin.spacing == null || !skin.spacing.isBound(); 809 } 810 811 @Override 812 public StyleableProperty<Number> getStyleableProperty(ToolBar n) { 813 final ToolBarSkin skin = (ToolBarSkin) n.getSkin(); 814 return (StyleableProperty<Number>)(WritableValue<Number>)skin.spacingProperty(); 815 } 816 }; 817 818 private static final CssMetaData<ToolBar,Pos>ALIGNMENT = 819 new CssMetaData<ToolBar,Pos>("-fx-alignment", 820 new EnumConverter<Pos>(Pos.class), Pos.TOP_LEFT ) { 821 822 @Override 823 public boolean isSettable(ToolBar n) { 824 final ToolBarSkin skin = (ToolBarSkin) n.getSkin(); 825 return skin.boxAlignment == null || !skin.boxAlignment.isBound(); 826 } 827 828 @Override 829 public StyleableProperty<Pos> getStyleableProperty(ToolBar n) { 830 final ToolBarSkin skin = (ToolBarSkin) n.getSkin(); 831 return (StyleableProperty<Pos>)(WritableValue<Pos>)skin.boxAlignmentProperty(); 832 } 833 }; 834 835 836 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 837 static { 838 839 final List<CssMetaData<? extends Styleable, ?>> styleables = 840 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData()); 841 842 // StackPane also has -fx-alignment. Replace it with 843 // ToolBarSkin's. 844 // TODO: Really should be able to reference StackPane.StyleableProperties.ALIGNMENT 845 final String alignmentProperty = ALIGNMENT.getProperty(); 846 for (int n=0, nMax=styleables.size(); n<nMax; n++) { 847 final CssMetaData<?,?> prop = styleables.get(n); 848 if (alignmentProperty.equals(prop.getProperty())) styleables.remove(prop); 849 } 850 851 styleables.add(SPACING); 852 styleables.add(ALIGNMENT); 853 STYLEABLES = Collections.unmodifiableList(styleables); 854 855 } 856 } 857 858 /** 859 * Returns the CssMetaData associated with this class, which may include the 860 * CssMetaData of its superclasses. 861 * @return the CssMetaData associated with this class, which may include the 862 * CssMetaData of its superclasses 863 */ 864 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 865 return StyleableProperties.STYLEABLES; 866 } 867 868 /** 869 * {@inheritDoc} 870 */ 871 @Override 872 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 873 return getClassCssMetaData(); 874 } 875 876 @Override 877 protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 878 switch (attribute) { 879 case OVERFLOW_BUTTON: return overflowMenu; 880 default: return super.queryAccessibleAttribute(attribute, parameters); 881 } 882 } 883 884 @Override 885 protected void executeAccessibleAction(AccessibleAction action, Object... parameters) { 886 switch (action) { 887 case SHOW_MENU: 888 overflowMenu.fire(); 889 break; 890 default: super.executeAccessibleAction(action, parameters); 891 } 892 } 893 }