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      */
 175     public final void setExpandedPane(TitledPane value) { expandedPaneProperty().set(value); }
 176 
 177     /**
 178      * Gets the expanded TitledPane in the Accordion.  If the expanded pane has been
 179      * removed or there is no expanded TitledPane {@code null} is returned.
 180      *
 181      * @return The expanded TitledPane in the Accordion.
 182      */
 183     public final TitledPane getExpandedPane() { return expandedPane.get(); }
 184 
 185     /**
 186      * The expanded TitledPane in the Accordion.
 187      *
 188      * @return The expanded TitledPane in the Accordion.
 189      */
 190     public final ObjectProperty<TitledPane> expandedPaneProperty() { return expandedPane; }
 191 
 192     /***************************************************************************
 193      *                                                                         *
 194      * Public API                                                              *
 195      *                                                                         *
 196      **************************************************************************/
 197 
 198     /**
 199      * Gets the list of {@link TitledPane} in this Accordion.  Changing this ObservableList
 200      * will immediately result in the Accordion updating to display
 201      * the new contents of this ObservableList.
 202      *
 203      * @return The list of TitledPane in this Accordion.
 204      */
 205     public final ObservableList<TitledPane> getPanes() { return panes; }
 206 
 207     /** {@inheritDoc} */
 208     @Override protected Skin<?> createDefaultSkin() {
 209         return new AccordionSkin(this);
 210     }
 211 
 212     /** {@inheritDoc} */
 213     @Override public void requestLayout() {
 214         biasDirty = true;
 215         bias = null;
 216         super.requestLayout();
 217     }
 218 
 219     /** {@inheritDoc} */
 220     @Override public Orientation getContentBias() {
 221         if (biasDirty) {
 222             bias = null;
 223             final List<Node> children = getManagedChildren();
 224             for (Node child : children) {
 225                 Orientation contentBias = child.getContentBias();
 226                 if (contentBias != null) {
 227                     bias = contentBias;
 228                     if (contentBias == Orientation.HORIZONTAL) {
 229                         break;
 230                     }
 231                 }
 232             }
 233             biasDirty = false;
 234         }
 235         return bias;
 236     }
 237 
 238     /***************************************************************************
 239      *                                                                         *
 240      * Stylesheet Handling                                                     *
 241      *                                                                         *
 242      **************************************************************************/
 243 
 244     private static final String DEFAULT_STYLE_CLASS = "accordion";
 245 
 246     /**
 247      * Returns the initial focus traversable state of this control, for use
 248      * by the JavaFX CSS engine to correctly set its initial value. This method
 249      * is overridden as by default UI controls have focus traversable set to true,
 250      * but that is not appropriate for this control.
 251      *
 252      * @since 9
 253      */
 254     @Override protected Boolean getInitialFocusTraversable() {
 255         return Boolean.FALSE;
 256     }
 257 
 258 }