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