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 }