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