1 /*
   2  * Copyright (c) 2010, 2016, 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;
  27 
  28 import javafx.beans.property.ObjectProperty;
  29 import javafx.collections.ListChangeListener;
  30 import javafx.collections.ObservableList;
  31 
  32 import com.sun.javafx.collections.TrackableObservableList;
  33 import javafx.geometry.Orientation;
  34 import javafx.scene.Node;
  35 import javafx.scene.control.skin.AccordionSkin;
  36 
  37 import javafx.beans.property.ObjectPropertyBase;
  38 import javafx.beans.value.WritableValue;
  39 import javafx.css.StyleableProperty;
  40 
  41 import java.util.List;
  42 
  43 /**
  44  * <p>An accordion is a group of {@link TitledPane TitlePanes}.  Only one TitledPane can be opened at
  45  * a time.</p>
  46  *
  47  * <p>The {@link TitledPane} content in an accordion can be any {@link javafx.scene.Node} such as UI controls or groups
  48  * of nodes added to a layout container.</p>
  49  *
  50  * <p>It is not recommended to set the MinHeight, PrefHeight, or MaxHeight
  51  * for this control.  Unexpected behavior will occur because the
  52  * Accordion's height changes when a TitledPane is opened or closed.</p>
  53  *
  54  * <p>
  55  * Accordion sets focusTraversable to false.
  56  * </p>
  57  *
  58  * <p>Example:
  59  * <pre><code>
  60  * TitledPane t1 = new TitledPane("T1", new Button("B1"));
  61  * TitledPane t2 = new TitledPane("T2", new Button("B2"));
  62  * TitledPane t3 = new TitledPane("T3", new Button("B3"));
  63  * Accordion accordion = new Accordion();
  64  * accordion.getPanes().addAll(t1, t2, t3);</code></pre>
  65  * @since JavaFX 2.0
  66  */
  67 public class Accordion extends Control {
  68     private boolean biasDirty = true;
  69     private Orientation bias;
  70 
  71     /***************************************************************************
  72      *                                                                         *
  73      * Constructors                                                            *
  74      *                                                                         *
  75      **************************************************************************/
  76 
  77     /**
  78      * Creates a new Accordion with no TitledPanes.
  79      */
  80     public Accordion() {
  81         this((TitledPane[])null);
  82     }
  83 
  84     /**
  85      * Creates a new Accordion with the given TitledPanes showing within it.
  86      *
  87      * @param titledPanes The TitledPanes to show inside the Accordion.
  88      * @since JavaFX 8u40
  89      */
  90     public Accordion(TitledPane... titledPanes) {
  91         getStyleClass().setAll(DEFAULT_STYLE_CLASS);
  92 
  93         if (titledPanes != null) {
  94             getPanes().addAll(titledPanes);
  95         }
  96 
  97         // focusTraversable is styleable through css. Calling setFocusTraversable
  98         // makes it look to css like the user set the value and css will not
  99         // override. Initializing focusTraversable by calling applyStyle with null
 100         // StyleOrigin ensures that css will be able to override the value.
 101         ((StyleableProperty<Boolean>)(WritableValue<Boolean>)focusTraversableProperty()).applyStyle(null, Boolean.FALSE);
 102     }
 103 
 104     /***************************************************************************
 105      *                                                                         *
 106      * Instance Variables                                                      *
 107      *                                                                         *
 108      **************************************************************************/
 109 
 110     // The ObservableList of TitlePanes to use in this Accordion.
 111     private final ObservableList<TitledPane> panes = new TrackableObservableList<TitledPane>() {
 112         @Override protected void onChanged(ListChangeListener.Change<TitledPane> c) {
 113             // If one of the removed panes was the expandedPane, then clear
 114             // the expandedPane property. This can only be done if expandedPane
 115             // is not bound (if it is bound, we just have to accept the
 116             // potential error condition and allow the skin to handle it).
 117             while (c.next()) {
 118                 if (c.wasRemoved() && !expandedPane.isBound()) {
 119                     for (TitledPane pane : c.getRemoved()) {
 120                         if (!c.getAddedSubList().contains(pane) && getExpandedPane() == pane) {
 121                             setExpandedPane(null);
 122                             break; // There can be only one, so no point continuing iteration
 123                         }
 124                     }
 125                 }
 126             }
 127         }
 128     };
 129 
 130     /***************************************************************************
 131      *                                                                         *
 132      * Properties                                                              *
 133      *                                                                         *
 134      **************************************************************************/
 135 
 136     // --- Expanded Pane
 137     private ObjectProperty<TitledPane> expandedPane = new ObjectPropertyBase<TitledPane>() {
 138 
 139         private TitledPane oldValue;
 140 
 141         @Override
 142         protected void invalidated() {
 143             final TitledPane value = get();
 144             if (value != null) {
 145                 value.setExpanded(true);
 146             } else {
 147                 if (oldValue != null) {
 148                     oldValue.setExpanded(false);
 149                 }
 150             }
 151             oldValue = value;
 152         }
 153 
 154         @Override
 155         public String getName() {
 156             return "expandedPane";
 157         }
 158 
 159         @Override
 160         public Object getBean() {
 161             return Accordion.this;
 162         }
 163 
 164     };
 165 
 166     /**
 167      * <p>The expanded {@link TitledPane} that is currently visible. While it is technically
 168      * possible to set the expanded pane to a value that is not in {@link #getPanes},
 169      * doing so will be treated by the skin as if expandedPane is null. If a pane
 170      * is set as the expanded pane, and is subsequently removed from {@link #getPanes},
 171      * then expanded pane will be set to null, if possible. (This will not be possible
 172      * if you have manually bound the expanded pane to some value, for example).
 173      * </p>
 174      * @param value the expanded {@link TitledPane}
 175      */
 176     public final void setExpandedPane(TitledPane value) { expandedPaneProperty().set(value); }
 177 
 178     /**
 179      * Gets the expanded TitledPane in the Accordion.  If the expanded pane has been
 180      * removed or there is no expanded TitledPane {@code null} is returned.
 181      *
 182      * @return The expanded TitledPane in the Accordion.
 183      */
 184     public final TitledPane getExpandedPane() { return expandedPane.get(); }
 185 
 186     /**
 187      * The expanded TitledPane in the Accordion.
 188      *
 189      * @return The expanded TitledPane in the Accordion.
 190      */
 191     public final ObjectProperty<TitledPane> expandedPaneProperty() { return expandedPane; }
 192 
 193     /***************************************************************************
 194      *                                                                         *
 195      * Public API                                                              *
 196      *                                                                         *
 197      **************************************************************************/
 198 
 199     /**
 200      * Gets the list of {@link TitledPane} in this Accordion.  Changing this ObservableList
 201      * will immediately result in the Accordion updating to display
 202      * the new contents of this ObservableList.
 203      *
 204      * @return The list of TitledPane in this Accordion.
 205      */
 206     public final ObservableList<TitledPane> getPanes() { return panes; }
 207 
 208     /** {@inheritDoc} */
 209     @Override protected Skin<?> createDefaultSkin() {
 210         return new AccordionSkin(this);
 211     }
 212 
 213     /** {@inheritDoc} */
 214     @Override public void requestLayout() {
 215         biasDirty = true;
 216         bias = null;
 217         super.requestLayout();
 218     }
 219 
 220     /** {@inheritDoc} */
 221     @Override public Orientation getContentBias() {
 222         if (biasDirty) {
 223             bias = null;
 224             final List<Node> children = getManagedChildren();
 225             for (Node child : children) {
 226                 Orientation contentBias = child.getContentBias();
 227                 if (contentBias != null) {
 228                     bias = contentBias;
 229                     if (contentBias == Orientation.HORIZONTAL) {
 230                         break;
 231                     }
 232                 }
 233             }
 234             biasDirty = false;
 235         }
 236         return bias;
 237     }
 238 
 239     /***************************************************************************
 240      *                                                                         *
 241      * Stylesheet Handling                                                     *
 242      *                                                                         *
 243      **************************************************************************/
 244 
 245     private static final String DEFAULT_STYLE_CLASS = "accordion";
 246 
 247     /**
 248      * Returns the initial focus traversable state of this control, for use
 249      * by the JavaFX CSS engine to correctly set its initial value. This method
 250      * is overridden as by default UI controls have focus traversable set to true,
 251      * but that is not appropriate for this control.
 252      *
 253      * @since 9
 254      */
 255     @Override protected Boolean getInitialFocusTraversable() {
 256         return Boolean.FALSE;
 257     }
 258 
 259 }