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 }