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.PlatformUtil; 29 import com.sun.javafx.scene.control.behavior.BehaviorBase; 30 import com.sun.javafx.scene.control.skin.Utils; 31 import javafx.animation.Animation.Status; 32 import javafx.animation.Interpolator; 33 import javafx.animation.KeyFrame; 34 import javafx.animation.KeyValue; 35 import javafx.animation.Timeline; 36 import javafx.beans.property.DoubleProperty; 37 import javafx.beans.property.SimpleDoubleProperty; 38 import javafx.geometry.HPos; 39 import javafx.geometry.Pos; 40 import javafx.geometry.VPos; 41 import javafx.scene.Cursor; 42 import javafx.scene.Node; 43 import javafx.scene.control.Button; 44 import javafx.scene.control.ContentDisplay; 45 import javafx.scene.control.Control; 46 import javafx.scene.control.TitledPane; 47 import javafx.scene.layout.StackPane; 48 import javafx.scene.shape.Rectangle; 49 import javafx.util.Duration; 50 51 import com.sun.javafx.scene.control.behavior.TitledPaneBehavior; 52 import javafx.beans.binding.DoubleBinding; 53 import javafx.geometry.Insets; 54 import javafx.scene.control.Accordion; 55 import javafx.scene.control.Labeled; 56 import javafx.scene.control.ContextMenu; 57 import javafx.scene.input.MouseButton; 58 import javafx.scene.text.Font; 59 60 /** 61 * Default skin implementation for the {@link TitledPane} control. 62 * 63 * @see TitledPane 64 * @since 9 65 */ 66 public class TitledPaneSkin extends LabeledSkinBase<TitledPane> { 67 68 /*************************************************************************** 69 * * 70 * Static fields * 71 * * 72 **************************************************************************/ 73 74 private static final Duration TRANSITION_DURATION = new Duration(350.0); 75 76 // caching results in poorer looking text (it is blurry), so we don't do it 77 // unless on a low powered device (admittedly the test below isn't a great 78 // indicator of power, but it'll do for now). 79 private static final boolean CACHE_ANIMATION = PlatformUtil.isEmbedded(); 80 81 82 83 /*************************************************************************** 84 * * 85 * Private fields * 86 * * 87 **************************************************************************/ 88 89 private final TitledPaneBehavior behavior; 90 91 private final TitleRegion titleRegion; 92 private final StackPane contentContainer; 93 private Node content; 94 private Timeline timeline; 95 private double transitionStartValue; 96 private Rectangle clipRect; 97 private Pos pos; 98 private HPos hpos; 99 private VPos vpos; 100 101 102 103 /*************************************************************************** 104 * * 105 * Constructors * 106 * * 107 **************************************************************************/ 108 109 /** 110 * Creates a new TitledPaneSkin instance, installing the necessary child 111 * nodes into the Control {@link Control#getChildren() children} list, as 112 * well as the necessary input mappings for handling key, mouse, etc events. 113 * 114 * @param control The control that this skin should be installed onto. 115 */ 116 public TitledPaneSkin(final TitledPane control) { 117 super(control); 118 119 // install default input map for the TitledPane control 120 this.behavior = new TitledPaneBehavior(control); 121 // control.setInputMap(behavior.getInputMap()); 122 123 clipRect = new Rectangle(); 124 125 transitionStartValue = 0; 126 titleRegion = new TitleRegion(); 127 128 content = getSkinnable().getContent(); 129 contentContainer = new StackPane() { 130 { 131 getStyleClass().setAll("content"); 132 133 if (content != null) { 134 getChildren().setAll(content); 135 } 136 } 137 }; 138 contentContainer.setClip(clipRect); 139 updateClip(); 140 141 if (control.isExpanded()) { 142 setTransition(1.0f); 143 setExpanded(control.isExpanded()); 144 } else { 145 setTransition(0.0f); 146 if (content != null) { 147 content.setVisible(false); 148 } 149 } 150 151 getChildren().setAll(contentContainer, titleRegion); 152 153 registerChangeListener(control.contentProperty(), e -> { 154 content = getSkinnable().getContent(); 155 if (content == null) { 156 contentContainer.getChildren().clear(); 157 } else { 158 contentContainer.getChildren().setAll(content); 159 } 160 }); 161 registerChangeListener(control.expandedProperty(), e -> setExpanded(getSkinnable().isExpanded())); 162 registerChangeListener(control.collapsibleProperty(), e -> titleRegion.update()); 163 registerChangeListener(control.alignmentProperty(), e -> { 164 pos = getSkinnable().getAlignment(); 165 hpos = pos.getHpos(); 166 vpos = pos.getVpos(); 167 }); 168 registerChangeListener(control.widthProperty(), e -> updateClip()); 169 registerChangeListener(control.heightProperty(), e -> updateClip()); 170 registerChangeListener(titleRegion.alignmentProperty(), e -> { 171 pos = titleRegion.getAlignment(); 172 hpos = pos.getHpos(); 173 vpos = pos.getVpos(); 174 }); 175 176 pos = control.getAlignment(); 177 hpos = pos == null ? HPos.LEFT : pos.getHpos(); 178 vpos = pos == null ? VPos.CENTER : pos.getVpos(); 179 } 180 181 182 183 /*************************************************************************** 184 * * 185 * Properties * 186 * * 187 **************************************************************************/ 188 189 private DoubleProperty transition; 190 private final void setTransition(double value) { transitionProperty().set(value); } 191 private final double getTransition() { return transition == null ? 0.0 : transition.get(); } 192 private final DoubleProperty transitionProperty() { 193 if (transition == null) { 194 transition = new SimpleDoubleProperty(this, "transition", 0.0) { 195 @Override protected void invalidated() { 196 contentContainer.requestLayout(); 197 } 198 }; 199 } 200 return transition; 201 } 202 203 204 205 /*************************************************************************** 206 * * 207 * Public API * 208 * * 209 **************************************************************************/ 210 211 /** {@inheritDoc} */ 212 @Override public void dispose() { 213 super.dispose(); 214 215 if (behavior != null) { 216 behavior.dispose(); 217 } 218 } 219 220 // Override LabeledSkinBase updateChildren because 221 // it removes all the children. The update() in TitleRegion 222 // will replace this method. 223 /** {@inheritDoc} */ 224 @Override protected void updateChildren() { 225 if (titleRegion != null) { 226 titleRegion.update(); 227 } 228 } 229 230 /** {@inheritDoc} */ 231 @Override protected void layoutChildren(final double x, double y, 232 final double w, final double h) { 233 234 // header 235 double headerHeight = snapSizeY(titleRegion.prefHeight(-1)); 236 237 titleRegion.resize(w, headerHeight); 238 positionInArea(titleRegion, x, y, 239 w, headerHeight, 0, HPos.LEFT, VPos.CENTER); 240 titleRegion.requestLayout(); 241 242 // content 243 double contentHeight = (h - headerHeight) * getTransition(); 244 if (isInsideAccordion()) { 245 if (prefHeightFromAccordion != 0) { 246 contentHeight = (prefHeightFromAccordion - headerHeight) * getTransition(); 247 } 248 } 249 contentHeight = snapSizeY(contentHeight); 250 251 // Header height was already snapped above. Is this just in case 252 // mods are made to the intervening code? Or is it just redundant? 253 y += snapSizeY(headerHeight); 254 contentContainer.resize(w, contentHeight); 255 clipRect.setHeight(contentHeight); 256 positionInArea(contentContainer, x, y, 257 w, contentHeight, /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 258 } 259 260 /** {@inheritDoc} */ 261 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 262 double titleWidth = snapSizeX(titleRegion.prefWidth(height)); 263 double contentWidth = snapSizeX(contentContainer.minWidth(height)); 264 return Math.max(titleWidth, contentWidth) + leftInset + rightInset; 265 } 266 267 /** {@inheritDoc} */ 268 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 269 double headerHeight = snapSizeY(titleRegion.prefHeight(width)); 270 double contentHeight = contentContainer.minHeight(width) * getTransition(); 271 return headerHeight + snapSizeY(contentHeight) + topInset + bottomInset; 272 } 273 274 /** {@inheritDoc} */ 275 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 276 double titleWidth = snapSizeX(titleRegion.prefWidth(height)); 277 double contentWidth = snapSizeX(contentContainer.prefWidth(height)); 278 return Math.max(titleWidth, contentWidth) + leftInset + rightInset; 279 } 280 281 /** {@inheritDoc} */ 282 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 283 double headerHeight = snapSizeY(titleRegion.prefHeight(width)); 284 double contentHeight = contentContainer.prefHeight(width) * getTransition(); 285 return headerHeight + snapSizeY(contentHeight) + topInset + bottomInset; 286 } 287 288 /** {@inheritDoc} */ 289 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 290 return Double.MAX_VALUE; 291 } 292 293 294 295 /*************************************************************************** 296 * * 297 * Private implementation * 298 * * 299 **************************************************************************/ 300 301 private void updateClip() { 302 clipRect.setWidth(getSkinnable().getWidth()); 303 clipRect.setHeight(contentContainer.getHeight()); 304 } 305 306 private void setExpanded(boolean expanded) { 307 if (! getSkinnable().isCollapsible()) { 308 setTransition(1.0f); 309 return; 310 } 311 312 // we need to perform the transition between expanded / hidden 313 if (getSkinnable().isAnimated()) { 314 transitionStartValue = getTransition(); 315 doAnimationTransition(); 316 } else { 317 if (expanded) { 318 setTransition(1.0f); 319 } else { 320 setTransition(0.0f); 321 } 322 if (content != null) { 323 content.setVisible(expanded); 324 } 325 getSkinnable().requestLayout(); 326 } 327 } 328 329 private boolean isInsideAccordion() { 330 return getSkinnable().getParent() != null && getSkinnable().getParent() instanceof Accordion; 331 } 332 333 double getTitleRegionSize(double width) { 334 return snapSizeY(titleRegion.prefHeight(width)) + snappedTopInset() + snappedBottomInset(); 335 } 336 337 private double prefHeightFromAccordion = 0; 338 void setMaxTitledPaneHeightForAccordion(double height) { 339 this.prefHeightFromAccordion = height; 340 } 341 342 double getTitledPaneHeightForAccordion() { 343 double headerHeight = snapSizeY(titleRegion.prefHeight(-1)); 344 double contentHeight = (prefHeightFromAccordion - headerHeight) * getTransition(); 345 return headerHeight + snapSizeY(contentHeight) + snappedTopInset() + snappedBottomInset(); 346 } 347 348 private void doAnimationTransition() { 349 if (content == null) { 350 return; 351 } 352 353 Duration duration; 354 if (timeline != null && (timeline.getStatus() != Status.STOPPED)) { 355 duration = timeline.getCurrentTime(); 356 timeline.stop(); 357 } else { 358 duration = TRANSITION_DURATION; 359 } 360 361 timeline = new Timeline(); 362 timeline.setCycleCount(1); 363 364 KeyFrame k1, k2; 365 366 if (getSkinnable().isExpanded()) { 367 k1 = new KeyFrame( 368 Duration.ZERO, 369 event -> { 370 // start expand 371 if (CACHE_ANIMATION) content.setCache(true); 372 content.setVisible(true); 373 }, 374 new KeyValue(transitionProperty(), transitionStartValue) 375 ); 376 377 k2 = new KeyFrame( 378 duration, 379 event -> { 380 // end expand 381 if (CACHE_ANIMATION) content.setCache(false); 382 }, 383 new KeyValue(transitionProperty(), 1, Interpolator.LINEAR) 384 385 ); 386 } else { 387 k1 = new KeyFrame( 388 Duration.ZERO, 389 event -> { 390 // Start collapse 391 if (CACHE_ANIMATION) content.setCache(true); 392 }, 393 new KeyValue(transitionProperty(), transitionStartValue) 394 ); 395 396 k2 = new KeyFrame( 397 duration, 398 event -> { 399 // end collapse 400 content.setVisible(false); 401 if (CACHE_ANIMATION) content.setCache(false); 402 }, 403 new KeyValue(transitionProperty(), 0, Interpolator.LINEAR) 404 ); 405 } 406 407 timeline.getKeyFrames().setAll(k1, k2); 408 timeline.play(); 409 } 410 411 412 413 /*************************************************************************** 414 * * 415 * Support classes * 416 * * 417 **************************************************************************/ 418 419 class TitleRegion extends StackPane { 420 private final StackPane arrowRegion; 421 422 public TitleRegion() { 423 getStyleClass().setAll("title"); 424 arrowRegion = new StackPane(); 425 arrowRegion.setId("arrowRegion"); 426 arrowRegion.getStyleClass().setAll("arrow-button"); 427 428 StackPane arrow = new StackPane(); 429 arrow.setId("arrow"); 430 arrow.getStyleClass().setAll("arrow"); 431 arrowRegion.getChildren().setAll(arrow); 432 433 // RT-13294: TitledPane : add animation to the title arrow 434 arrow.rotateProperty().bind(new DoubleBinding() { 435 { bind(transitionProperty()); } 436 437 @Override protected double computeValue() { 438 return -90 * (1.0 - getTransition()); 439 } 440 }); 441 442 setAlignment(Pos.CENTER_LEFT); 443 444 setOnMouseReleased(e -> { 445 if( e.getButton() != MouseButton.PRIMARY ) return; 446 ContextMenu contextMenu = getSkinnable().getContextMenu() ; 447 if (contextMenu != null) { 448 contextMenu.hide() ; 449 } 450 if (getSkinnable().isCollapsible() && getSkinnable().isFocused()) { 451 behavior.toggle(); 452 } 453 }); 454 455 // title region consists of the title and the arrow regions 456 update(); 457 } 458 459 private void update() { 460 getChildren().clear(); 461 final TitledPane titledPane = getSkinnable(); 462 463 if (titledPane.isCollapsible()) { 464 getChildren().add(arrowRegion); 465 } 466 467 // Only in some situations do we want to have the graphicPropertyChangedListener 468 // installed. Since updateChildren() is not called much, we'll just remove it always 469 // and reinstall it later if it is necessary to do so. 470 if (graphic != null) { 471 graphic.layoutBoundsProperty().removeListener(graphicPropertyChangedListener); 472 } 473 // Now update the graphic (since it may have changed) 474 graphic = titledPane.getGraphic(); 475 // Now update the children (and add the graphicPropertyChangedListener as necessary) 476 if (isIgnoreGraphic()) { 477 if (titledPane.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY) { 478 getChildren().clear(); 479 getChildren().add(arrowRegion); 480 } else { 481 getChildren().add(text); 482 } 483 } else { 484 graphic.layoutBoundsProperty().addListener(graphicPropertyChangedListener); 485 if (isIgnoreText()) { 486 getChildren().add(graphic); 487 } else { 488 getChildren().addAll(graphic, text); 489 } 490 } 491 setCursor(getSkinnable().isCollapsible() ? Cursor.HAND : Cursor.DEFAULT); 492 } 493 494 @Override protected double computePrefWidth(double height) { 495 double left = snappedLeftInset(); 496 double right = snappedRightInset(); 497 double arrowWidth = 0; 498 double labelPrefWidth = labelPrefWidth(height); 499 500 if (arrowRegion != null) { 501 arrowWidth = snapSize(arrowRegion.prefWidth(height)); 502 } 503 504 return left + arrowWidth + labelPrefWidth + right; 505 } 506 507 @Override protected double computePrefHeight(double width) { 508 double top = snappedTopInset(); 509 double bottom = snappedBottomInset(); 510 double arrowHeight = 0; 511 double labelPrefHeight = labelPrefHeight(width); 512 513 if (arrowRegion != null) { 514 arrowHeight = snapSize(arrowRegion.prefHeight(width)); 515 } 516 517 return top + Math.max(arrowHeight, labelPrefHeight) + bottom; 518 } 519 520 @Override protected void layoutChildren() { 521 final double top = snappedTopInset(); 522 final double bottom = snappedBottomInset(); 523 final double left = snappedLeftInset(); 524 final double right = snappedRightInset(); 525 double width = getWidth() - (left + right); 526 double height = getHeight() - (top + bottom); 527 double arrowWidth = snapSize(arrowRegion.prefWidth(-1)); 528 double arrowHeight = snapSize(arrowRegion.prefHeight(-1)); 529 double labelWidth = snapSize(Math.min(width - arrowWidth / 2.0, labelPrefWidth(-1))); 530 double labelHeight = snapSize(labelPrefHeight(-1)); 531 532 double x = left + arrowWidth + Utils.computeXOffset(width - arrowWidth, labelWidth, hpos); 533 if (HPos.CENTER == hpos) { 534 // We want to center the region based on the entire width of the TitledPane. 535 x = left + Utils.computeXOffset(width, labelWidth, hpos); 536 } 537 double y = top + Utils.computeYOffset(height, Math.max(arrowHeight, labelHeight), vpos); 538 539 arrowRegion.resize(arrowWidth, arrowHeight); 540 positionInArea(arrowRegion, left, top, arrowWidth, height, 541 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 542 543 layoutLabelInArea(x, y, labelWidth, height, pos); 544 } 545 546 // Copied from LabeledSkinBase because the padding from TitledPane was being 547 // applied to the Label when it should not be. 548 private double labelPrefWidth(double height) { 549 // Get the preferred width of the text 550 final Labeled labeled = getSkinnable(); 551 final Font font = text.getFont(); 552 final String string = labeled.getText(); 553 boolean emptyText = string == null || string.isEmpty(); 554 Insets labelPadding = labeled.getLabelPadding(); 555 double widthPadding = labelPadding.getLeft() + labelPadding.getRight(); 556 double textWidth = emptyText ? 0 : Utils.computeTextWidth(font, string, 0); 557 558 // Now add on the graphic, gap, and padding as appropriate 559 final Node graphic = labeled.getGraphic(); 560 if (isIgnoreGraphic()) { 561 return textWidth + widthPadding; 562 } else if (isIgnoreText()) { 563 return graphic.prefWidth(-1) + widthPadding; 564 } else if (labeled.getContentDisplay() == ContentDisplay.LEFT 565 || labeled.getContentDisplay() == ContentDisplay.RIGHT) { 566 return textWidth + labeled.getGraphicTextGap() + graphic.prefWidth(-1) + widthPadding; 567 } else { 568 return Math.max(textWidth, graphic.prefWidth(-1)) + widthPadding; 569 } 570 } 571 572 // Copied from LabeledSkinBase because the padding from TitledPane was being 573 // applied to the Label when it should not be. 574 private double labelPrefHeight(double width) { 575 final Labeled labeled = getSkinnable(); 576 final Font font = text.getFont(); 577 final ContentDisplay contentDisplay = labeled.getContentDisplay(); 578 final double gap = labeled.getGraphicTextGap(); 579 final Insets labelPadding = labeled.getLabelPadding(); 580 final double widthPadding = snappedLeftInset() + snappedRightInset() + labelPadding.getLeft() + labelPadding.getRight(); 581 582 String str = labeled.getText(); 583 if (str != null && str.endsWith("\n")) { 584 // Strip ending newline so we don't count another row. 585 str = str.substring(0, str.length() - 1); 586 } 587 588 if (!isIgnoreGraphic() && 589 (contentDisplay == ContentDisplay.LEFT || contentDisplay == ContentDisplay.RIGHT)) { 590 width -= (graphic.prefWidth(-1) + gap); 591 } 592 593 width -= widthPadding; 594 595 // TODO figure out how to cache this effectively. 596 final double textHeight = Utils.computeTextHeight(font, str, 597 labeled.isWrapText() ? width : 0, text.getBoundsType()); 598 599 // Now we want to add on the graphic if necessary! 600 double h = textHeight; 601 if (!isIgnoreGraphic()) { 602 final Node graphic = labeled.getGraphic(); 603 if (contentDisplay == ContentDisplay.TOP || contentDisplay == ContentDisplay.BOTTOM) { 604 h = graphic.prefHeight(-1) + gap + textHeight; 605 } else { 606 h = Math.max(textHeight, graphic.prefHeight(-1)); 607 } 608 } 609 610 return h + labelPadding.getTop() + labelPadding.getBottom(); 611 } 612 } 613 }