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 = snapSize(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 = snapSize(contentHeight); 250 251 y += snapSize(headerHeight); 252 contentContainer.resize(w, contentHeight); 253 clipRect.setHeight(contentHeight); 254 positionInArea(contentContainer, x, y, 255 w, contentHeight, /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 256 } 257 258 /** {@inheritDoc} */ 259 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 260 double titleWidth = snapSize(titleRegion.prefWidth(height)); 261 double contentWidth = snapSize(contentContainer.minWidth(height)); 262 return Math.max(titleWidth, contentWidth) + leftInset + rightInset; 263 } 264 265 /** {@inheritDoc} */ 266 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 267 double headerHeight = snapSize(titleRegion.prefHeight(width)); 268 double contentHeight = contentContainer.minHeight(width) * getTransition(); 269 return headerHeight + snapSize(contentHeight) + topInset + bottomInset; 270 } 271 272 /** {@inheritDoc} */ 273 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 274 double titleWidth = snapSize(titleRegion.prefWidth(height)); 275 double contentWidth = snapSize(contentContainer.prefWidth(height)); 276 return Math.max(titleWidth, contentWidth) + leftInset + rightInset; 277 } 278 279 /** {@inheritDoc} */ 280 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 281 double headerHeight = snapSize(titleRegion.prefHeight(width)); 282 double contentHeight = contentContainer.prefHeight(width) * getTransition(); 283 return headerHeight + snapSize(contentHeight) + topInset + bottomInset; 284 } 285 286 /** {@inheritDoc} */ 287 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 288 return Double.MAX_VALUE; 289 } 290 291 292 293 /*************************************************************************** 294 * * 295 * Private implementation * 296 * * 297 **************************************************************************/ 298 299 private void updateClip() { 300 clipRect.setWidth(getSkinnable().getWidth()); 301 clipRect.setHeight(contentContainer.getHeight()); 302 } 303 304 private void setExpanded(boolean expanded) { 305 if (! getSkinnable().isCollapsible()) { 306 setTransition(1.0f); 307 return; 308 } 309 310 // we need to perform the transition between expanded / hidden 311 if (getSkinnable().isAnimated()) { 312 transitionStartValue = getTransition(); 313 doAnimationTransition(); 314 } else { 315 if (expanded) { 316 setTransition(1.0f); 317 } else { 318 setTransition(0.0f); 319 } 320 if (content != null) { 321 content.setVisible(expanded); 322 } 323 getSkinnable().requestLayout(); 324 } 325 } 326 327 private boolean isInsideAccordion() { 328 return getSkinnable().getParent() != null && getSkinnable().getParent() instanceof Accordion; 329 } 330 331 double getTitleRegionSize(double width) { 332 return snapSize(titleRegion.prefHeight(width)) + snappedTopInset() + snappedBottomInset(); 333 } 334 335 private double prefHeightFromAccordion = 0; 336 void setMaxTitledPaneHeightForAccordion(double height) { 337 this.prefHeightFromAccordion = height; 338 } 339 340 double getTitledPaneHeightForAccordion() { 341 double headerHeight = snapSize(titleRegion.prefHeight(-1)); 342 double contentHeight = (prefHeightFromAccordion - headerHeight) * getTransition(); 343 return headerHeight + snapSize(contentHeight) + snappedTopInset() + snappedBottomInset(); 344 } 345 346 private void doAnimationTransition() { 347 if (content == null) { 348 return; 349 } 350 351 Duration duration; 352 if (timeline != null && (timeline.getStatus() != Status.STOPPED)) { 353 duration = timeline.getCurrentTime(); 354 timeline.stop(); 355 } else { 356 duration = TRANSITION_DURATION; 357 } 358 359 timeline = new Timeline(); 360 timeline.setCycleCount(1); 361 362 KeyFrame k1, k2; 363 364 if (getSkinnable().isExpanded()) { 365 k1 = new KeyFrame( 366 Duration.ZERO, 367 event -> { 368 // start expand 369 if (CACHE_ANIMATION) content.setCache(true); 370 content.setVisible(true); 371 }, 372 new KeyValue(transitionProperty(), transitionStartValue) 373 ); 374 375 k2 = new KeyFrame( 376 duration, 377 event -> { 378 // end expand 379 if (CACHE_ANIMATION) content.setCache(false); 380 }, 381 new KeyValue(transitionProperty(), 1, Interpolator.LINEAR) 382 383 ); 384 } else { 385 k1 = new KeyFrame( 386 Duration.ZERO, 387 event -> { 388 // Start collapse 389 if (CACHE_ANIMATION) content.setCache(true); 390 }, 391 new KeyValue(transitionProperty(), transitionStartValue) 392 ); 393 394 k2 = new KeyFrame( 395 duration, 396 event -> { 397 // end collapse 398 content.setVisible(false); 399 if (CACHE_ANIMATION) content.setCache(false); 400 }, 401 new KeyValue(transitionProperty(), 0, Interpolator.LINEAR) 402 ); 403 } 404 405 timeline.getKeyFrames().setAll(k1, k2); 406 timeline.play(); 407 } 408 409 410 411 /*************************************************************************** 412 * * 413 * Support classes * 414 * * 415 **************************************************************************/ 416 417 class TitleRegion extends StackPane { 418 private final StackPane arrowRegion; 419 420 public TitleRegion() { 421 getStyleClass().setAll("title"); 422 arrowRegion = new StackPane(); 423 arrowRegion.setId("arrowRegion"); 424 arrowRegion.getStyleClass().setAll("arrow-button"); 425 426 StackPane arrow = new StackPane(); 427 arrow.setId("arrow"); 428 arrow.getStyleClass().setAll("arrow"); 429 arrowRegion.getChildren().setAll(arrow); 430 431 // RT-13294: TitledPane : add animation to the title arrow 432 arrow.rotateProperty().bind(new DoubleBinding() { 433 { bind(transitionProperty()); } 434 435 @Override protected double computeValue() { 436 return -90 * (1.0 - getTransition()); 437 } 438 }); 439 440 setAlignment(Pos.CENTER_LEFT); 441 442 setOnMouseReleased(e -> { 443 if( e.getButton() != MouseButton.PRIMARY ) return; 444 ContextMenu contextMenu = getSkinnable().getContextMenu() ; 445 if (contextMenu != null) { 446 contextMenu.hide() ; 447 } 448 if (getSkinnable().isCollapsible() && getSkinnable().isFocused()) { 449 behavior.toggle(); 450 } 451 }); 452 453 // title region consists of the title and the arrow regions 454 update(); 455 } 456 457 private void update() { 458 getChildren().clear(); 459 final TitledPane titledPane = getSkinnable(); 460 461 if (titledPane.isCollapsible()) { 462 getChildren().add(arrowRegion); 463 } 464 465 // Only in some situations do we want to have the graphicPropertyChangedListener 466 // installed. Since updateChildren() is not called much, we'll just remove it always 467 // and reinstall it later if it is necessary to do so. 468 if (graphic != null) { 469 graphic.layoutBoundsProperty().removeListener(graphicPropertyChangedListener); 470 } 471 // Now update the graphic (since it may have changed) 472 graphic = titledPane.getGraphic(); 473 // Now update the children (and add the graphicPropertyChangedListener as necessary) 474 if (isIgnoreGraphic()) { 475 if (titledPane.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY) { 476 getChildren().clear(); 477 getChildren().add(arrowRegion); 478 } else { 479 getChildren().add(text); 480 } 481 } else { 482 graphic.layoutBoundsProperty().addListener(graphicPropertyChangedListener); 483 if (isIgnoreText()) { 484 getChildren().add(graphic); 485 } else { 486 getChildren().addAll(graphic, text); 487 } 488 } 489 setCursor(getSkinnable().isCollapsible() ? Cursor.HAND : Cursor.DEFAULT); 490 } 491 492 @Override protected double computePrefWidth(double height) { 493 double left = snappedLeftInset(); 494 double right = snappedRightInset(); 495 double arrowWidth = 0; 496 double labelPrefWidth = labelPrefWidth(height); 497 498 if (arrowRegion != null) { 499 arrowWidth = snapSize(arrowRegion.prefWidth(height)); 500 } 501 502 return left + arrowWidth + labelPrefWidth + right; 503 } 504 505 @Override protected double computePrefHeight(double width) { 506 double top = snappedTopInset(); 507 double bottom = snappedBottomInset(); 508 double arrowHeight = 0; 509 double labelPrefHeight = labelPrefHeight(width); 510 511 if (arrowRegion != null) { 512 arrowHeight = snapSize(arrowRegion.prefHeight(width)); 513 } 514 515 return top + Math.max(arrowHeight, labelPrefHeight) + bottom; 516 } 517 518 @Override protected void layoutChildren() { 519 final double top = snappedTopInset(); 520 final double bottom = snappedBottomInset(); 521 final double left = snappedLeftInset(); 522 final double right = snappedRightInset(); 523 double width = getWidth() - (left + right); 524 double height = getHeight() - (top + bottom); 525 double arrowWidth = snapSize(arrowRegion.prefWidth(-1)); 526 double arrowHeight = snapSize(arrowRegion.prefHeight(-1)); 527 double labelWidth = snapSize(Math.min(width - arrowWidth / 2.0, labelPrefWidth(-1))); 528 double labelHeight = snapSize(labelPrefHeight(-1)); 529 530 double x = left + arrowWidth + Utils.computeXOffset(width - arrowWidth, labelWidth, hpos); 531 if (HPos.CENTER == hpos) { 532 // We want to center the region based on the entire width of the TitledPane. 533 x = left + Utils.computeXOffset(width, labelWidth, hpos); 534 } 535 double y = top + Utils.computeYOffset(height, Math.max(arrowHeight, labelHeight), vpos); 536 537 arrowRegion.resize(arrowWidth, arrowHeight); 538 positionInArea(arrowRegion, left, top, arrowWidth, height, 539 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 540 541 layoutLabelInArea(x, y, labelWidth, height, pos); 542 } 543 544 // Copied from LabeledSkinBase because the padding from TitledPane was being 545 // applied to the Label when it should not be. 546 private double labelPrefWidth(double height) { 547 // Get the preferred width of the text 548 final Labeled labeled = getSkinnable(); 549 final Font font = text.getFont(); 550 final String string = labeled.getText(); 551 boolean emptyText = string == null || string.isEmpty(); 552 Insets labelPadding = labeled.getLabelPadding(); 553 double widthPadding = labelPadding.getLeft() + labelPadding.getRight(); 554 double textWidth = emptyText ? 0 : Utils.computeTextWidth(font, string, 0); 555 556 // Now add on the graphic, gap, and padding as appropriate 557 final Node graphic = labeled.getGraphic(); 558 if (isIgnoreGraphic()) { 559 return textWidth + widthPadding; 560 } else if (isIgnoreText()) { 561 return graphic.prefWidth(-1) + widthPadding; 562 } else if (labeled.getContentDisplay() == ContentDisplay.LEFT 563 || labeled.getContentDisplay() == ContentDisplay.RIGHT) { 564 return textWidth + labeled.getGraphicTextGap() + graphic.prefWidth(-1) + widthPadding; 565 } else { 566 return Math.max(textWidth, graphic.prefWidth(-1)) + widthPadding; 567 } 568 } 569 570 // Copied from LabeledSkinBase because the padding from TitledPane was being 571 // applied to the Label when it should not be. 572 private double labelPrefHeight(double width) { 573 final Labeled labeled = getSkinnable(); 574 final Font font = text.getFont(); 575 final ContentDisplay contentDisplay = labeled.getContentDisplay(); 576 final double gap = labeled.getGraphicTextGap(); 577 final Insets labelPadding = labeled.getLabelPadding(); 578 final double widthPadding = snappedLeftInset() + snappedRightInset() + labelPadding.getLeft() + labelPadding.getRight(); 579 580 String str = labeled.getText(); 581 if (str != null && str.endsWith("\n")) { 582 // Strip ending newline so we don't count another row. 583 str = str.substring(0, str.length() - 1); 584 } 585 586 if (!isIgnoreGraphic() && 587 (contentDisplay == ContentDisplay.LEFT || contentDisplay == ContentDisplay.RIGHT)) { 588 width -= (graphic.prefWidth(-1) + gap); 589 } 590 591 width -= widthPadding; 592 593 // TODO figure out how to cache this effectively. 594 final double textHeight = Utils.computeTextHeight(font, str, 595 labeled.isWrapText() ? width : 0, text.getBoundsType()); 596 597 // Now we want to add on the graphic if necessary! 598 double h = textHeight; 599 if (!isIgnoreGraphic()) { 600 final Node graphic = labeled.getGraphic(); 601 if (contentDisplay == ContentDisplay.TOP || contentDisplay == ContentDisplay.BOTTOM) { 602 h = graphic.prefHeight(-1) + gap + textHeight; 603 } else { 604 h = Math.max(textHeight, graphic.prefHeight(-1)); 605 } 606 } 607 608 return h + labelPadding.getTop() + labelPadding.getBottom(); 609 } 610 } 611 }