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 com.sun.javafx.scene.control.Properties; 29 import com.sun.javafx.scene.control.behavior.BehaviorBase; 30 import javafx.beans.value.ObservableValue; 31 import javafx.geometry.Orientation; 32 import javafx.geometry.Point2D; 33 import javafx.scene.AccessibleAction; 34 import javafx.scene.AccessibleAttribute; 35 import javafx.scene.AccessibleRole; 36 import javafx.scene.control.Accordion; 37 import javafx.scene.control.Button; 38 import javafx.scene.control.Control; 39 import javafx.scene.control.ScrollBar; 40 import javafx.scene.control.SkinBase; 41 import javafx.scene.input.MouseButton; 42 import javafx.scene.input.ScrollEvent; 43 import javafx.scene.layout.Region; 44 import javafx.scene.layout.StackPane; 45 import javafx.scene.Node; 46 import com.sun.javafx.util.Utils; 47 import com.sun.javafx.scene.control.behavior.ScrollBarBehavior; 48 49 import java.util.function.Consumer; 50 51 /** 52 * Default skin implementation for the {@link ScrollBar} control. 53 * 54 * @see ScrollBar 55 * @since 9 56 */ 57 public class ScrollBarSkin extends SkinBase<ScrollBar> { 58 59 /*************************************************************************** 60 * * 61 * Private fields * 62 * * 63 **************************************************************************/ 64 65 private final ScrollBarBehavior behavior; 66 67 private StackPane thumb; 68 private StackPane trackBackground; 69 private StackPane track; 70 private EndButton incButton; 71 private EndButton decButton; 72 73 private double trackLength; 74 private double thumbLength; 75 76 private double preDragThumbPos; 77 private Point2D dragStart; // in the track's coord system 78 79 private double trackPos; 80 81 82 83 /*************************************************************************** 84 * * 85 * Constructors * 86 * * 87 **************************************************************************/ 88 89 /** 90 * Creates a new ScrollBarSkin instance, installing the necessary child 91 * nodes into the Control {@link Control#getChildren() children} list, as 92 * well as the necessary input mappings for handling key, mouse, etc events. 93 * 94 * @param control The control that this skin should be installed onto. 95 */ 96 public ScrollBarSkin(ScrollBar control) { 97 super(control); 98 99 // install default input map for the ScrollBar control 100 this.behavior = new ScrollBarBehavior(control); 101 // control.setInputMap(behavior.getInputMap()); 102 103 initialize(); 104 getSkinnable().requestLayout(); 105 106 // Register listeners 107 final Consumer<ObservableValue<?>> consumer = e -> { 108 positionThumb(); 109 getSkinnable().requestLayout(); 110 }; 111 registerChangeListener(control.minProperty(), consumer); 112 registerChangeListener(control.maxProperty(), consumer); 113 registerChangeListener(control.visibleAmountProperty(), consumer); 114 registerChangeListener(control.valueProperty(), e -> positionThumb()); 115 registerChangeListener(control.orientationProperty(), e -> getSkinnable().requestLayout()); 116 } 117 118 119 120 /*************************************************************************** 121 * * 122 * Public API * 123 * * 124 **************************************************************************/ 125 126 /** {@inheritDoc} */ 127 @Override public void dispose() { 128 super.dispose(); 129 130 if (behavior != null) { 131 behavior.dispose(); 132 } 133 } 134 135 /** {@inheritDoc} */ 136 @Override protected void layoutChildren(final double x, final double y, 137 final double w, final double h) { 138 139 final ScrollBar s = getSkinnable(); 140 141 /** 142 * Compute the percentage length of thumb as (visibleAmount/range) 143 * if max isn't greater than min then there is nothing to do here 144 */ 145 double visiblePortion; 146 if (s.getMax() > s.getMin()) { 147 visiblePortion = s.getVisibleAmount()/(s.getMax() - s.getMin()); 148 } 149 else { 150 visiblePortion = 1.0; 151 } 152 153 if (s.getOrientation() == Orientation.VERTICAL) { 154 if (!Properties.IS_TOUCH_SUPPORTED) { 155 double decHeight = snapSize(decButton.prefHeight(-1)); 156 double incHeight = snapSize(incButton.prefHeight(-1)); 157 158 decButton.resize(w, decHeight); 159 incButton.resize(w, incHeight); 160 161 trackLength = snapSize(h - (decHeight + incHeight)); 162 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength)); 163 164 trackBackground.resizeRelocate(snapPosition(x), snapPosition(y), w, trackLength+decHeight+incHeight); 165 decButton.relocate(snapPosition(x), snapPosition(y)); 166 incButton.relocate(snapPosition(x), snapPosition(y + h - incHeight)); 167 track.resizeRelocate(snapPosition(x), snapPosition(y + decHeight), w, trackLength); 168 thumb.resize(snapSize(x >= 0 ? w : w + x), thumbLength); // Account for negative padding (see also RT-10719) 169 positionThumb(); 170 } 171 else { 172 trackLength = snapSize(h); 173 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength)); 174 175 track.resizeRelocate(snapPosition(x), snapPosition(y), w, trackLength); 176 thumb.resize(snapSize(x >= 0 ? w : w + x), thumbLength); // Account for negative padding (see also RT-10719) 177 positionThumb(); 178 } 179 } else { 180 if (!Properties.IS_TOUCH_SUPPORTED) { 181 double decWidth = snapSize(decButton.prefWidth(-1)); 182 double incWidth = snapSize(incButton.prefWidth(-1)); 183 184 decButton.resize(decWidth, h); 185 incButton.resize(incWidth, h); 186 187 trackLength = snapSize(w - (decWidth + incWidth)); 188 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength)); 189 190 trackBackground.resizeRelocate(snapPosition(x), snapPosition(y), trackLength+decWidth+incWidth, h); 191 decButton.relocate(snapPosition(x), snapPosition(y)); 192 incButton.relocate(snapPosition(x + w - incWidth), snapPosition(y)); 193 track.resizeRelocate(snapPosition(x + decWidth), snapPosition(y), trackLength, h); 194 thumb.resize(thumbLength, snapSize(y >= 0 ? h : h + y)); // Account for negative padding (see also RT-10719) 195 positionThumb(); 196 } 197 else { 198 trackLength = snapSize(w); 199 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength)); 200 201 track.resizeRelocate(snapPosition(x), snapPosition(y), trackLength, h); 202 thumb.resize(thumbLength, snapSize(y >= 0 ? h : h + y)); // Account for negative padding (see also RT-10719) 203 positionThumb(); 204 } 205 206 s.resize(snapSize(s.getWidth()), snapSize(s.getHeight())); 207 } 208 209 // things should be invisible only when well below minimum length 210 if (s.getOrientation() == Orientation.VERTICAL && h >= (computeMinHeight(-1, (int)y , snappedRightInset(), snappedBottomInset(), (int)x) - (y+snappedBottomInset())) || 211 s.getOrientation() == Orientation.HORIZONTAL && w >= (computeMinWidth(-1, (int)y , snappedRightInset(), snappedBottomInset(), (int)x) - (x+snappedRightInset()))) { 212 trackBackground.setVisible(true); 213 track.setVisible(true); 214 thumb.setVisible(true); 215 if (!Properties.IS_TOUCH_SUPPORTED) { 216 incButton.setVisible(true); 217 decButton.setVisible(true); 218 } 219 } 220 else { 221 trackBackground.setVisible(false); 222 track.setVisible(false); 223 thumb.setVisible(false); 224 225 if (!Properties.IS_TOUCH_SUPPORTED) { 226 /* 227 ** once the space is big enough for one button we 228 ** can look at drawing 229 */ 230 if (h >= decButton.computeMinWidth(-1)) { 231 decButton.setVisible(true); 232 } 233 else { 234 decButton.setVisible(false); 235 } 236 if (h >= incButton.computeMinWidth(-1)) { 237 incButton.setVisible(true); 238 } 239 else { 240 incButton.setVisible(false); 241 } 242 } 243 } 244 } 245 246 /* 247 * Minimum length is the length of the end buttons plus twice the 248 * minimum thumb length, which should be enough for a reasonably-sized 249 * track. Minimum breadth is determined by the breadths of the 250 * end buttons. 251 */ 252 /** {@inheritDoc} */ 253 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 254 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 255 return getBreadth(); 256 } else { 257 if (!Properties.IS_TOUCH_SUPPORTED) { 258 return decButton.minWidth(-1) + incButton.minWidth(-1) + minTrackLength()+leftInset+rightInset; 259 } else { 260 return minTrackLength()+leftInset+rightInset; 261 } 262 } 263 } 264 265 /** {@inheritDoc} */ 266 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 267 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 268 if (!Properties.IS_TOUCH_SUPPORTED) { 269 return decButton.minHeight(-1) + incButton.minHeight(-1) + minTrackLength()+topInset+bottomInset; 270 } else { 271 return minTrackLength()+topInset+bottomInset; 272 } 273 } else { 274 return getBreadth(); 275 } 276 } 277 278 /* 279 * Preferred size. The breadth is determined by the breadth of 280 * the end buttons. The length is a constant default length. 281 * Usually applications or other components will either set a 282 * specific length using LayoutInfo or will stretch the length 283 * of the scrollbar to fit a container. 284 */ 285 /** {@inheritDoc} */ 286 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 287 final ScrollBar s = getSkinnable(); 288 return s.getOrientation() == Orientation.VERTICAL ? getBreadth() : Properties.DEFAULT_LENGTH+leftInset+rightInset; 289 } 290 291 /** {@inheritDoc} */ 292 @Override protected double computePrefHeight(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 293 final ScrollBar s = getSkinnable(); 294 return s.getOrientation() == Orientation.VERTICAL ? Properties.DEFAULT_LENGTH+topInset+bottomInset : getBreadth(); 295 } 296 297 /** {@inheritDoc} */ 298 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 299 final ScrollBar s = getSkinnable(); 300 return s.getOrientation() == Orientation.VERTICAL ? s.prefWidth(-1) : Double.MAX_VALUE; 301 } 302 303 /** {@inheritDoc} */ 304 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 305 final ScrollBar s = getSkinnable(); 306 return s.getOrientation() == Orientation.VERTICAL ? Double.MAX_VALUE : s.prefHeight(-1); 307 } 308 309 310 311 /*************************************************************************** 312 * * 313 * Private implementation * 314 * * 315 **************************************************************************/ 316 317 /** 318 * Initializes the ScrollBarSkin. Creates the scene and sets up all the 319 * bindings for the group. 320 */ 321 private void initialize() { 322 323 track = new StackPane(); 324 track.getStyleClass().setAll("track"); 325 326 trackBackground = new StackPane(); 327 trackBackground.getStyleClass().setAll("track-background"); 328 329 thumb = new StackPane() { 330 @Override 331 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 332 switch (attribute) { 333 case VALUE: return getSkinnable().getValue(); 334 default: return super.queryAccessibleAttribute(attribute, parameters); 335 } 336 } 337 }; 338 thumb.getStyleClass().setAll("thumb"); 339 thumb.setAccessibleRole(AccessibleRole.THUMB); 340 341 342 if (!Properties.IS_TOUCH_SUPPORTED) { 343 344 incButton = new EndButton("increment-button", "increment-arrow") { 345 @Override 346 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 347 switch (action) { 348 case FIRE: 349 getSkinnable().increment(); 350 break; 351 default: super.executeAccessibleAction(action, parameters); 352 } 353 } 354 }; 355 incButton.setAccessibleRole(AccessibleRole.INCREMENT_BUTTON); 356 incButton.setOnMousePressed(me -> { 357 /* 358 ** if the tracklenght isn't greater than do nothing.... 359 */ 360 if (!thumb.isVisible() || trackLength > thumbLength) { 361 behavior.incButtonPressed(); 362 } 363 me.consume(); 364 }); 365 incButton.setOnMouseReleased(me -> { 366 /* 367 ** if the tracklenght isn't greater than do nothing.... 368 */ 369 if (!thumb.isVisible() || trackLength > thumbLength) { 370 behavior.incButtonReleased(); 371 } 372 me.consume(); 373 }); 374 375 decButton = new EndButton("decrement-button", "decrement-arrow") { 376 @Override 377 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 378 switch (action) { 379 case FIRE: 380 getSkinnable().decrement(); 381 break; 382 default: super.executeAccessibleAction(action, parameters); 383 } 384 } 385 }; 386 decButton.setAccessibleRole(AccessibleRole.DECREMENT_BUTTON); 387 decButton.setOnMousePressed(me -> { 388 /* 389 ** if the tracklenght isn't greater than do nothing.... 390 */ 391 if (!thumb.isVisible() || trackLength > thumbLength) { 392 behavior.decButtonPressed(); 393 } 394 me.consume(); 395 }); 396 decButton.setOnMouseReleased(me -> { 397 /* 398 ** if the tracklenght isn't greater than do nothing.... 399 */ 400 if (!thumb.isVisible() || trackLength > thumbLength) { 401 behavior.decButtonReleased(); 402 } 403 me.consume(); 404 }); 405 } 406 407 408 track.setOnMousePressed(me -> { 409 if (!thumb.isPressed() && me.getButton() == MouseButton.PRIMARY) { 410 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 411 if (trackLength != 0) { 412 behavior.trackPress(me.getY() / trackLength); 413 me.consume(); 414 } 415 } else { 416 if (trackLength != 0) { 417 behavior.trackPress(me.getX() / trackLength); 418 me.consume(); 419 } 420 } 421 } 422 }); 423 424 track.setOnMouseReleased(me -> { 425 behavior.trackRelease(); 426 me.consume(); 427 }); 428 429 thumb.setOnMousePressed(me -> { 430 if (me.isSynthesized()) { 431 // touch-screen events handled by Scroll handler 432 me.consume(); 433 return; 434 } 435 /* 436 ** if max isn't greater than min then there is nothing to do here 437 */ 438 if (getSkinnable().getMax() > getSkinnable().getMin()) { 439 dragStart = thumb.localToParent(me.getX(), me.getY()); 440 double clampedValue = Utils.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax()); 441 preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin()); 442 me.consume(); 443 } 444 }); 445 446 447 thumb.setOnMouseDragged(me -> { 448 if (me.isSynthesized()) { 449 // touch-screen events handled by Scroll handler 450 me.consume(); 451 return; 452 } 453 /* 454 ** if max isn't greater than min then there is nothing to do here 455 */ 456 if (getSkinnable().getMax() > getSkinnable().getMin()) { 457 /* 458 ** if the tracklength isn't greater then do nothing.... 459 */ 460 if (trackLength > thumbLength) { 461 Point2D cur = thumb.localToParent(me.getX(), me.getY()); 462 if (dragStart == null) { 463 // we're getting dragged without getting a mouse press 464 dragStart = thumb.localToParent(me.getX(), me.getY()); 465 } 466 double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX(); 467 behavior.thumbDragged(preDragThumbPos + dragPos / (trackLength - thumbLength)); 468 } 469 470 me.consume(); 471 } 472 }); 473 474 thumb.setOnScrollStarted(se -> { 475 if (se.isDirect()) { 476 /* 477 ** if max isn't greater than min then there is nothing to do here 478 */ 479 if (getSkinnable().getMax() > getSkinnable().getMin()) { 480 dragStart = thumb.localToParent(se.getX(), se.getY()); 481 double clampedValue = Utils.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax()); 482 preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin()); 483 se.consume(); 484 } 485 } 486 }); 487 488 thumb.setOnScroll(event -> { 489 if (event.isDirect()) { 490 /* 491 ** if max isn't greater than min then there is nothing to do here 492 */ 493 if (getSkinnable().getMax() > getSkinnable().getMin()) { 494 /* 495 ** if the tracklength isn't greater then do nothing.... 496 */ 497 if (trackLength > thumbLength) { 498 Point2D cur = thumb.localToParent(event.getX(), event.getY()); 499 if (dragStart == null) { 500 // we're getting dragged without getting a mouse press 501 dragStart = thumb.localToParent(event.getX(), event.getY()); 502 } 503 double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX(); 504 behavior.thumbDragged(/*todo*/ preDragThumbPos + dragPos / (trackLength - thumbLength)); 505 } 506 507 event.consume(); 508 return; 509 } 510 } 511 }); 512 513 514 getSkinnable().addEventHandler(ScrollEvent.SCROLL, event -> { 515 /* 516 ** if the tracklength isn't greater then do nothing.... 517 */ 518 if (trackLength > thumbLength) { 519 520 double dx = event.getDeltaX(); 521 double dy = event.getDeltaY(); 522 523 /* 524 ** in 2.0 a horizontal scrollbar would scroll on a vertical 525 ** drag on a tracker-pad. We need to keep this behavior. 526 */ 527 dx = (Math.abs(dx) < Math.abs(dy) ? dy : dx); 528 529 /* 530 ** we only consume an event that we've used. 531 */ 532 ScrollBar sb = (ScrollBar) getSkinnable(); 533 534 double delta = (getSkinnable().getOrientation() == Orientation.VERTICAL ? dy : dx); 535 536 /* 537 ** RT-22941 - If this is either a touch or inertia scroll 538 ** then we move to the position of the touch point. 539 * 540 * TODO: this fix causes RT-23406 ([ScrollBar, touch] Dragging scrollbar from the 541 * track on touchscreen causes flickering) 542 */ 543 if (event.isDirect()) { 544 if (trackLength > thumbLength) { 545 behavior.thumbDragged((getSkinnable().getOrientation() == Orientation.VERTICAL ? event.getY() : event.getX()) / trackLength); 546 event.consume(); 547 } 548 } 549 else { 550 if (delta > 0.0 && sb.getValue() > sb.getMin()) { 551 sb.decrement(); 552 event.consume(); 553 } else if (delta < 0.0 && sb.getValue() < sb.getMax()) { 554 sb.increment(); 555 event.consume(); 556 } 557 } 558 } 559 }); 560 561 getChildren().clear(); 562 if (!Properties.IS_TOUCH_SUPPORTED) { 563 getChildren().addAll(trackBackground, incButton, decButton, track, thumb); 564 } 565 else { 566 getChildren().addAll(track, thumb); 567 } 568 } 569 570 571 /* 572 * Gets the breadth of the scrollbar. The "breadth" is the distance 573 * across the scrollbar, i.e. if vertical the width, otherwise the height. 574 * On desktop this is determined by the greater of the breadths of the end-buttons. 575 * Embedded doesn't have end-buttons, so currently we use a default breadth. 576 * We should change this when we get width/height css properties. 577 */ 578 double getBreadth() { 579 if (!Properties.IS_TOUCH_SUPPORTED) { 580 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 581 return Math.max(decButton.prefWidth(-1), incButton.prefWidth(-1)) +snappedLeftInset()+snappedRightInset(); 582 } else { 583 return Math.max(decButton.prefHeight(-1), incButton.prefHeight(-1)) +snappedTopInset()+snappedBottomInset(); 584 } 585 } 586 else { 587 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 588 return Math.max(Properties.DEFAULT_EMBEDDED_SB_BREADTH, Properties.DEFAULT_EMBEDDED_SB_BREADTH)+snappedLeftInset()+snappedRightInset(); 589 } else { 590 return Math.max(Properties.DEFAULT_EMBEDDED_SB_BREADTH, Properties.DEFAULT_EMBEDDED_SB_BREADTH)+snappedTopInset()+snappedBottomInset(); 591 } 592 } 593 } 594 595 double minThumbLength() { 596 return 1.5f * getBreadth(); 597 } 598 599 double minTrackLength() { 600 return 2.0f * getBreadth(); 601 } 602 603 /** 604 * Called when ever either min, max or value changes, so thumb's layoutX, Y is recomputed. 605 */ 606 void positionThumb() { 607 ScrollBar s = getSkinnable(); 608 double clampedValue = Utils.clamp(s.getMin(), s.getValue(), s.getMax()); 609 trackPos = (s.getMax() - s.getMin() > 0) ? ((trackLength - thumbLength) * (clampedValue - s.getMin()) / (s.getMax() - s.getMin())) : (0.0F); 610 611 if (!Properties.IS_TOUCH_SUPPORTED) { 612 if (s.getOrientation() == Orientation.VERTICAL) { 613 trackPos += decButton.prefHeight(-1); 614 } else { 615 trackPos += decButton.prefWidth(-1); 616 } 617 } 618 619 thumb.setTranslateX( snapPosition(s.getOrientation() == Orientation.VERTICAL ? snappedLeftInset() : trackPos + snappedLeftInset())); 620 thumb.setTranslateY( snapPosition(s.getOrientation() == Orientation.VERTICAL ? trackPos + snappedTopInset() : snappedTopInset())); 621 } 622 623 private Node getThumb() { 624 return thumb; 625 } 626 627 private Node getTrack() { 628 return track; 629 } 630 631 private Node getIncrementButton() { 632 return incButton; 633 } 634 635 private Node getDecrementButton() { 636 return decButton; 637 } 638 639 640 641 /*************************************************************************** 642 * * 643 * Support classes * 644 * * 645 **************************************************************************/ 646 647 private static class EndButton extends Region { 648 private Region arrow; 649 650 private EndButton(String styleClass, String arrowStyleClass) { 651 getStyleClass().setAll(styleClass); 652 arrow = new Region(); 653 arrow.getStyleClass().setAll(arrowStyleClass); 654 getChildren().setAll(arrow); 655 requestLayout(); 656 } 657 658 @Override protected void layoutChildren() { 659 final double top = snappedTopInset(); 660 final double left = snappedLeftInset(); 661 final double bottom = snappedBottomInset(); 662 final double right = snappedRightInset(); 663 final double aw = snapSize(arrow.prefWidth(-1)); 664 final double ah = snapSize(arrow.prefHeight(-1)); 665 final double yPos = snapPosition((getHeight() - (top + bottom + ah)) / 2.0); 666 final double xPos = snapPosition((getWidth() - (left + right + aw)) / 2.0); 667 arrow.resizeRelocate(xPos + left, yPos + top, aw, ah); 668 } 669 670 @Override protected double computeMinHeight(double width) { 671 return prefHeight(-1); 672 } 673 674 @Override protected double computeMinWidth(double height) { 675 return prefWidth(-1); 676 } 677 678 @Override protected double computePrefWidth(double height) { 679 final double left = snappedLeftInset(); 680 final double right = snappedRightInset(); 681 final double aw = snapSize(arrow.prefWidth(-1)); 682 return left + aw + right; 683 } 684 685 @Override protected double computePrefHeight(double width) { 686 final double top = snappedTopInset(); 687 final double bottom = snappedBottomInset(); 688 final double ah = snapSize(arrow.prefHeight(-1)); 689 return top + ah + bottom; 690 } 691 } 692 }