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 += snapSizeY(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 = snapSizeY(((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 }