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.scene.control.behavior.BehaviorBase;
  29 import javafx.beans.value.ChangeListener;
  30 import javafx.collections.ListChangeListener;
  31 import javafx.scene.Node;
  32 import javafx.scene.control.Accordion;
  33 import javafx.scene.control.Button;
  34 import javafx.scene.control.Control;
  35 import javafx.scene.control.Skin;
  36 import javafx.scene.control.SkinBase;
  37 import javafx.scene.control.TitledPane;
  38 import javafx.scene.shape.Rectangle;
  39 
  40 import com.sun.javafx.scene.control.behavior.AccordionBehavior;
  41 
  42 import java.util.HashMap;
  43 import java.util.List;
  44 import java.util.Map;
  45 
  46 /**
  47  * Default skin implementation for the {@link Accordion} control.
  48  *
  49  * @see Accordion
  50  * @since 9
  51  */
  52 public class AccordionSkin extends SkinBase<Accordion> {
  53 
  54     /***************************************************************************
  55      *                                                                         *
  56      * Private fields                                                          *
  57      *                                                                         *
  58      **************************************************************************/
  59 
  60     private TitledPane firstTitledPane;
  61     private Rectangle clipRect;
  62 
  63     // This is used when we definitely want to force a relayout, regardless of
  64     // whether the height has also changed
  65     private boolean forceRelayout = false;
  66 
  67     // this is used to request a layout, assuming the height has also changed
  68     private boolean relayout = false;
  69 
  70     // we record the previous height to know if the current height is different
  71     private double previousHeight = 0;
  72 
  73     private TitledPane expandedPane = null;
  74     private TitledPane previousPane = null;
  75     private Map<TitledPane, ChangeListener<Boolean>>listeners = new HashMap<>();
  76 
  77     private final BehaviorBase<Accordion> behavior;
  78 
  79 
  80 
  81     /***************************************************************************
  82      *                                                                         *
  83      * Constructors                                                            *
  84      *                                                                         *
  85      **************************************************************************/
  86 
  87     /**
  88      * Creates a new AccordionSkin instance, installing the necessary child
  89      * nodes into the Control {@link Control#getChildren() children} list, as
  90      * well as the necessary input mappings for handling key, mouse, etc events.
  91      *
  92      * @param control The control that this skin should be installed onto.
  93      */
  94     public AccordionSkin(final Accordion control) {
  95         super(control);
  96 
  97         // install default input map for the accordion control
  98         behavior = new AccordionBehavior(control);
  99 //        control.setInputMap(behavior.getInputMap());
 100 
 101         control.getPanes().addListener((ListChangeListener<TitledPane>) c -> {
 102             if (firstTitledPane != null) {
 103                 firstTitledPane.getStyleClass().remove("first-titled-pane");
 104             }
 105             if (!control.getPanes().isEmpty()) {
 106                 firstTitledPane = control.getPanes().get(0);
 107                 firstTitledPane.getStyleClass().add("first-titled-pane");
 108             }
 109             // TODO there may be a more efficient way to keep these in sync
 110             getChildren().setAll(control.getPanes());
 111             while (c.next()) {
 112                 removeTitledPaneListeners(c.getRemoved());
 113                 initTitledPaneListeners(c.getAddedSubList());
 114             }
 115 
 116             // added to resolve RT-32787
 117             forceRelayout = true;
 118         });
 119 
 120         if (!control.getPanes().isEmpty()) {
 121             firstTitledPane = control.getPanes().get(0);
 122             firstTitledPane.getStyleClass().add("first-titled-pane");
 123         }
 124 
 125         clipRect = new Rectangle(control.getWidth(), control.getHeight());
 126         getSkinnable().setClip(clipRect);
 127 
 128         initTitledPaneListeners(control.getPanes());
 129         getChildren().setAll(control.getPanes());
 130         getSkinnable().requestLayout();
 131 
 132         registerChangeListener(getSkinnable().widthProperty(), e -> clipRect.setWidth(getSkinnable().getWidth()));
 133         registerChangeListener(getSkinnable().heightProperty(), e -> {
 134             clipRect.setHeight(getSkinnable().getHeight());
 135             relayout = true;
 136         });
 137     }
 138 
 139 
 140 
 141     /***************************************************************************
 142      *                                                                         *
 143      * Public API                                                              *
 144      *                                                                         *
 145      **************************************************************************/
 146 
 147     /** {@inheritDoc} */
 148     @Override public void dispose() {
 149         super.dispose();
 150 
 151         if (behavior != null) {
 152             behavior.dispose();
 153         }
 154     }
 155 
 156     /** {@inheritDoc} */
 157     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 158         double h = 0;
 159 
 160         if (expandedPane != null) {
 161             h += expandedPane.minHeight(width);
 162         }
 163 
 164         if (previousPane != null && !previousPane.equals(expandedPane)) {
 165             h += previousPane.minHeight(width);
 166         }
 167 
 168         for (Node child: getChildren()) {
 169             TitledPane pane = (TitledPane)child;
 170             if (!pane.equals(expandedPane) && !pane.equals(previousPane)) {
 171                 final Skin<?> skin = ((TitledPane)child).getSkin();
 172                 if (skin instanceof TitledPaneSkin) {
 173                     TitledPaneSkin childSkin = (TitledPaneSkin) skin;
 174                     h += childSkin.getTitleRegionSize(width);
 175                 } else {
 176                     h += pane.minHeight(width);
 177                 }
 178             }
 179         }
 180 
 181         return h + topInset + bottomInset;
 182     }
 183 
 184     /** {@inheritDoc} */
 185     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 186         double h = 0;
 187 
 188         if (expandedPane != null) {
 189             h += expandedPane.prefHeight(width);
 190         }
 191 
 192         if (previousPane != null && !previousPane.equals(expandedPane)) {
 193             h += previousPane.prefHeight(width);
 194         }
 195 
 196         for (Node child: getChildren()) {
 197             TitledPane pane = (TitledPane)child;
 198             if (!pane.equals(expandedPane) && !pane.equals(previousPane)) {
 199                 final Skin<?> skin = ((TitledPane)child).getSkin();
 200                 if (skin instanceof TitledPaneSkin) {
 201                     TitledPaneSkin childSkin = (TitledPaneSkin) skin;
 202                     h += childSkin.getTitleRegionSize(width);
 203                 } else {
 204                     h += pane.prefHeight(width);
 205                 }
 206             }
 207         }
 208 
 209         return h + topInset + bottomInset;
 210     }
 211 
 212     /** {@inheritDoc} */
 213     @Override protected void layoutChildren(final double x, double y,
 214             final double w, final double h) {
 215         final boolean rebuild = forceRelayout || (relayout && previousHeight != h);
 216         forceRelayout = false;
 217         previousHeight = h;
 218 
 219         // Compute height of all the collapsed panes
 220         double collapsedPanesHeight = 0;
 221         for (TitledPane tp : getSkinnable().getPanes()) {
 222             if (!tp.equals(expandedPane)) {
 223                 TitledPaneSkin childSkin = (TitledPaneSkin) ((TitledPane)tp).getSkin();
 224                 collapsedPanesHeight += snapSize(childSkin.getTitleRegionSize(w));
 225             }
 226         }
 227         final double maxTitledPaneHeight = h - collapsedPanesHeight;
 228 
 229         for (TitledPane tp : getSkinnable().getPanes()) {
 230             Skin<?> skin = tp.getSkin();
 231             double ph;
 232             if (skin instanceof TitledPaneSkin) {
 233                 ((TitledPaneSkin)skin).setMaxTitledPaneHeightForAccordion(maxTitledPaneHeight);
 234                 ph = snapSize(((TitledPaneSkin)skin).getTitledPaneHeightForAccordion());
 235             } else {
 236                 ph = tp.prefHeight(w);
 237             }
 238             tp.resize(w, ph);
 239 
 240             boolean needsRelocate = true;
 241             if (! rebuild && previousPane != null && expandedPane != null) {
 242                 List<TitledPane> panes = getSkinnable().getPanes();
 243                 final int previousPaneIndex = panes.indexOf(previousPane);
 244                 final int expandedPaneIndex = panes.indexOf(expandedPane);
 245                 final int currentPaneIndex  = panes.indexOf(tp);
 246 
 247                 if (previousPaneIndex < expandedPaneIndex) {
 248                     // Current expanded pane is after the previous expanded pane..
 249                     // Only move the panes that are less than or equal to the current expanded.
 250                     if (currentPaneIndex <= expandedPaneIndex) {
 251                         tp.relocate(x, y);
 252                         y += ph;
 253                         needsRelocate = false;
 254                     }
 255                 } else if (previousPaneIndex > expandedPaneIndex) {
 256                     // Previous pane is after the current expanded pane.
 257                     // Only move the panes that are less than or equal to the previous expanded pane.
 258                     if (currentPaneIndex <= previousPaneIndex) {
 259                         tp.relocate(x, y);
 260                         y += ph;
 261                         needsRelocate = false;
 262                     }
 263                 } else {
 264                     // Previous and current expanded pane are the same.
 265                     // Since we are expanding and collapsing the same pane we will need to relocate
 266                     // all the panes.
 267                     tp.relocate(x, y);
 268                     y += ph;
 269                     needsRelocate = false;
 270                 }
 271             }
 272 
 273             if (needsRelocate) {
 274                 tp.relocate(x, y);
 275                 y += ph;
 276             }
 277         }
 278     }
 279 
 280 
 281 
 282     /***************************************************************************
 283      *                                                                         *
 284      * Private implementation                                                  *
 285      *                                                                         *
 286      **************************************************************************/
 287 
 288     private void initTitledPaneListeners(List<? extends TitledPane> list) {
 289         for (final TitledPane tp: list) {
 290             tp.setExpanded(tp == getSkinnable().getExpandedPane());
 291             if (tp.isExpanded()) {
 292                 expandedPane = tp;
 293             }
 294             ChangeListener<Boolean> changeListener = expandedPropertyListener(tp);
 295             tp.expandedProperty().addListener(changeListener);
 296             listeners.put(tp, changeListener);
 297         }
 298     }
 299 
 300     private void removeTitledPaneListeners(List<? extends TitledPane> list) {
 301         for (final TitledPane tp: list) {
 302             if (listeners.containsKey(tp)) {
 303                 tp.expandedProperty().removeListener(listeners.get(tp));
 304                 listeners.remove(tp);
 305             }
 306         }
 307     }
 308 
 309     private ChangeListener<Boolean> expandedPropertyListener(final TitledPane tp) {
 310         return (observable, wasExpanded, expanded) -> {
 311             previousPane = expandedPane;
 312             final Accordion accordion = getSkinnable();
 313             if (expanded) {
 314                 if (expandedPane != null) {
 315                     expandedPane.setExpanded(false);
 316                 }
 317                 if (tp != null) {
 318                     accordion.setExpandedPane(tp);
 319                 }
 320                 expandedPane = accordion.getExpandedPane();
 321             } else {
 322                 expandedPane = null;
 323                 accordion.setExpandedPane(null);
 324             }
 325         };
 326     }
 327 }