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 }