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 }