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