1 /*
   2  * Copyright (c) 2012, 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 javafx.scene.control;
  27 
  28 import javafx.scene.control.cell.CheckBoxTreeCell;
  29 import javafx.beans.property.BooleanProperty;
  30 import javafx.beans.property.SimpleBooleanProperty;
  31 import javafx.beans.value.ChangeListener;
  32 import javafx.event.Event;
  33 import javafx.event.EventType;
  34 import javafx.scene.Node;
  35 
  36 /**
  37  * TreeItem subclass that adds support for being in selected, unselected, and
  38  * indeterminate states. This is useful when used in conjunction with a TreeView
  39  * which has a {@link CheckBoxTreeCell} installed.
  40  *
  41  * <p>A CheckBoxTreeItem can be {@link #independentProperty() independent} or
  42  * dependent. By default, CheckBoxTreeItem instances are dependent, which means
  43  * that any changes to the selection state of a TreeItem will have an impact on
  44  * parent and children CheckBoxTreeItem instances. If a CheckBoxTreeItem is
  45  * set to be independent, this means that any changes to that CheckBoxTreeItem
  46  * will not directly impact the state of parent and children CheckBoxTreeItem
  47  * instances.
  48  *
  49  * <p>The {@link #indeterminateProperty() indeterminate} property is used to
  50  * represent the same concept as that in {@link CheckBox#indeterminateProperty()},
  51  * namely, that the CheckBox is neither selected or unselected. This is commonly
  52  * used inside a TreeView when some, but not all, of a branches children are
  53  * selected.
  54  *
  55  * <p>A simple example of using the CheckBoxTreeItem class, in conjunction with
  56  * {@link CheckBoxTreeCell} is shown below:
  57  *
  58  * <pre><code>
  59  * // create the tree model
  60  * CheckBoxTreeItem&lt;String&gt; jonathanGiles = new CheckBoxTreeItem&lt;String&gt;("Jonathan");
  61  * CheckBoxTreeItem&lt;String&gt; juliaGiles = new CheckBoxTreeItem&lt;String&gt;("Julia");
  62  * CheckBoxTreeItem&lt;String&gt; mattGiles = new CheckBoxTreeItem&lt;String&gt;("Matt");
  63  * CheckBoxTreeItem&lt;String&gt; sueGiles = new CheckBoxTreeItem&lt;String&gt;("Sue");
  64  * CheckBoxTreeItem&lt;String&gt; ianGiles = new CheckBoxTreeItem&lt;String&gt;("Ian");
  65  *
  66  * CheckBoxTreeItem&lt;String&gt; gilesFamily = new CheckBoxTreeItem&lt;String&gt;("Giles Family");
  67  * gilesFamily.setExpanded(true);
  68  * gilesFamily.getChildren().addAll(jonathanGiles, juliaGiles, mattGiles, sueGiles, ianGiles);
  69  *
  70  * // create the treeView
  71  * final TreeView&lt;String&gt; treeView = new TreeView&lt;String&gt;();
  72  * treeView.setRoot(gilesFamily);
  73  *
  74  * // set the cell factory
  75  * treeView.setCellFactory(CheckBoxTreeCell.&lt;String&gt;forTreeView());</code></pre>
  76  *
  77  * @param <T> The type of the value contained within the TreeItem
  78  * @see CheckBoxTreeCell
  79  * @see TreeItem
  80  * @see CheckBox
  81  * @since JavaFX 2.2
  82  */
  83 // TODO the TreeModificationEvent doesn't actually bubble in the same way as
  84 // TreeItem - it just looks that way as the 'bubbling' is done via changing the
  85 // state of all parent items.
  86 public class CheckBoxTreeItem<T> extends TreeItem<T> {
  87 
  88     /**
  89      * An EventType used when the CheckBoxTreeItem selection / indeterminate
  90      * state changes. To use this, it is recommended that you use code along the
  91      * lines of the following:
  92      *
  93      *<pre>
  94      * {@code
  95      * child1.addEventHandler(CheckBoxTreeItem.<String>checkBoxSelectionChangedEvent(), new EventHandler<TreeModificationEvent<String>>() {
  96      *     public void handle(TreeModificationEvent<String> event) {
  97      *          ...
  98      *     }
  99      * });}
 100      * </pre>
 101      *
 102      * @param <T> The type of the value contained within the TreeItem.
 103      * @return the EventType used when the CheckBoxTreeItem selection / indeterminate
 104      * state changes
 105      */
 106     @SuppressWarnings("unchecked")
 107     public static <T> EventType<TreeModificationEvent<T>> checkBoxSelectionChangedEvent() {
 108         return (EventType<TreeModificationEvent<T>>) CHECK_BOX_SELECTION_CHANGED_EVENT;
 109     }
 110     private static final EventType<? extends Event> CHECK_BOX_SELECTION_CHANGED_EVENT
 111             = new EventType<Event>(TreeModificationEvent.ANY, "checkBoxSelectionChangedEvent");
 112 
 113     /***************************************************************************
 114      *                                                                         *
 115      * Constructors                                                            *
 116      *                                                                         *
 117      **************************************************************************/
 118 
 119     /**
 120      * Creates an empty CheckBoxTreeItem.
 121      */
 122     public CheckBoxTreeItem() {
 123         this(null);
 124     }
 125 
 126     /**
 127      * Creates a CheckBoxTreeItem with the value property set to the provided
 128      * object.
 129      *
 130      * @param value The object to be stored as the value of this TreeItem.
 131      */
 132     public CheckBoxTreeItem(T value) {
 133         this(value, null, false);
 134     }
 135 
 136     /**
 137      * Creates a CheckBoxTreeItem with the value property set to the provided
 138      * object, and the graphic set to the provided Node.
 139      *
 140      * @param value The object to be stored as the value of this CheckBoxTreeItem.
 141      * @param graphic The Node to show in the TreeView next to this CheckBoxTreeItem.
 142      */
 143     public CheckBoxTreeItem(T value, Node graphic) {
 144         this(value, graphic, false);
 145     }
 146 
 147     /**
 148      * Creates a CheckBoxTreeItem with the value property set to the provided
 149      * object, the graphic set to the provided Node, and the initial state
 150      * of the {@link #selectedProperty()} set to the provided boolean value.
 151      *
 152      * @param value The object to be stored as the value of this CheckBoxTreeItem.
 153      * @param graphic The Node to show in the TreeView next to this CheckBoxTreeItem.
 154      * @param selected The initial value of the
 155      *            {@link #selectedProperty() selected} property.
 156      */
 157     public CheckBoxTreeItem(T value, Node graphic, boolean selected) {
 158         this(value, graphic, selected, false);
 159     }
 160 
 161     /**
 162      * Creates a CheckBoxTreeItem with the value property set to the provided
 163      * object, the graphic set to the provided Node, the initial state
 164      * of the {@link #selectedProperty()} set to the provided boolean value, and
 165      * the initial state of the {@link #independentProperty() independent}
 166      * property to the provided boolean value.
 167      *
 168      * @param value The object to be stored as the value of this CheckBoxTreeItem.
 169      * @param graphic The Node to show in the TreeView next to this CheckBoxTreeItem.
 170      * @param selected The initial value of the
 171      *            {@link #selectedProperty() selected} property.
 172      * @param independent The initial value of the
 173      *            {@link #independentProperty() independent} property
 174      */
 175     public CheckBoxTreeItem(T value, Node graphic, boolean selected, boolean independent) {
 176         super(value, graphic);
 177         setSelected(selected);
 178         setIndependent(independent);
 179 
 180         selectedProperty().addListener(stateChangeListener);
 181         indeterminateProperty().addListener(stateChangeListener);
 182     }
 183 
 184 
 185 
 186     /***************************************************************************
 187      *                                                                         *
 188      * Callbacks                                                               *
 189      *                                                                         *
 190      **************************************************************************/
 191     private final ChangeListener<Boolean> stateChangeListener = (ov, oldVal, newVal) -> {
 192         updateState();
 193     };
 194 
 195 
 196     /***************************************************************************
 197      *                                                                         *
 198      * Properties                                                              *
 199      *                                                                         *
 200      **************************************************************************/
 201 
 202     // --- Selected
 203     private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected", false) {
 204         @Override protected void invalidated() {
 205             super.invalidated();
 206             fireEvent(CheckBoxTreeItem.this, true);
 207         }
 208     };
 209 
 210     /**
 211      * Sets the selected state of this CheckBoxTreeItem.
 212      * @param value the selected state of this CheckBoxTreeItem
 213      */
 214     public final void setSelected(boolean value) { selectedProperty().setValue(value); }
 215 
 216     /**
 217      * Returns the selected state of this CheckBoxTreeItem.
 218      * @return true if CheckBoxTreeItem is selected
 219      */
 220     public final boolean isSelected() { return selected.getValue(); }
 221 
 222     /**
 223      * A {@link BooleanProperty} used to represent the selected state of this CheckBoxTreeItem.
 224      * @return the selected state property of this CheckBoxTreeItem
 225      */
 226     public final BooleanProperty selectedProperty() { return selected; }
 227 
 228 
 229     // --- Indeterminate
 230     private final BooleanProperty indeterminate = new SimpleBooleanProperty(this, "indeterminate", false) {
 231         @Override protected void invalidated() {
 232             super.invalidated();
 233             fireEvent(CheckBoxTreeItem.this, false);
 234         }
 235     };
 236 
 237     /**
 238      * Sets the indeterminate state of this CheckBoxTreeItem.
 239      * @param value the indeterminate state of this CheckBoxTreeItem
 240      */
 241     public final void setIndeterminate(boolean value) { indeterminateProperty().setValue(value); }
 242 
 243     /**
 244      * Returns the indeterminate state of this CheckBoxTreeItem.
 245      * @return true if CheckBoxTreeItem is indeterminate state
 246      */
 247     public final boolean isIndeterminate() { return indeterminate.getValue(); }
 248 
 249     /**
 250      * A {@link BooleanProperty} used to represent the indeterminate state of this CheckBoxTreeItem.
 251      * @return the indeterminate state property of this CheckBoxTreeItem
 252      */
 253     public final BooleanProperty indeterminateProperty() { return indeterminate; }
 254 
 255 
 256     // --- Independent
 257     /**
 258      * A {@link BooleanProperty} used to represent the independent state of this CheckBoxTreeItem.
 259      * The independent state is used to represent whether changes to a single
 260      * CheckBoxTreeItem should influence the state of its parent and children.
 261      *
 262      * <p>By default, the independent property is false, which means that when
 263      * a CheckBoxTreeItem has state changes to the selected or indeterminate
 264      * properties, the state of related CheckBoxTreeItems will possibly be changed.
 265      * If the independent property is set to true, the state of related CheckBoxTreeItems
 266      * will <b>never</b> change.
 267      * @return the independent state property of this CheckBoxTreeItem
 268      */
 269     public final BooleanProperty independentProperty() { return independent; }
 270     private final BooleanProperty independent = new SimpleBooleanProperty(this, "independent", false);
 271     public final void setIndependent(boolean value) { independentProperty().setValue(value); }
 272     public final boolean isIndependent() { return independent.getValue(); }
 273 
 274 
 275 
 276     /***************************************************************************
 277      *                                                                         *
 278      * Private Implementation                                                  *
 279      *                                                                         *
 280      **************************************************************************/
 281 
 282     private static boolean updateLock = false;
 283 
 284     private void updateState() {
 285         if (isIndependent()) return;
 286 
 287         boolean firstLock = ! updateLock;
 288 
 289         // toggle parent (recursively up to root)
 290         updateLock = true;
 291         updateUpwards();
 292 
 293         if (firstLock) updateLock = false;
 294 
 295         // toggle children
 296         if (updateLock) return;
 297         updateDownwards();
 298     }
 299 
 300     private void updateUpwards() {
 301         if (! (getParent() instanceof CheckBoxTreeItem)) return;
 302 
 303         CheckBoxTreeItem<?> parent = (CheckBoxTreeItem<?>) getParent();
 304         int selectCount = 0;
 305         int indeterminateCount = 0;
 306         for (TreeItem<?> child : parent.getChildren()) {
 307             if (! (child instanceof CheckBoxTreeItem)) continue;
 308 
 309             CheckBoxTreeItem<?> cbti = (CheckBoxTreeItem<?>) child;
 310 
 311             selectCount += cbti.isSelected() && ! cbti.isIndeterminate() ? 1 : 0;
 312             indeterminateCount += cbti.isIndeterminate() ? 1 : 0;
 313         }
 314 
 315         if (selectCount == parent.getChildren().size()) {
 316             parent.setSelected(true);
 317             parent.setIndeterminate(false);
 318         } else if (selectCount == 0 && indeterminateCount == 0) {
 319             parent.setSelected(false);
 320             parent.setIndeterminate(false);
 321         } else {
 322             parent.setIndeterminate(true);
 323         }
 324     }
 325 
 326     private void updateDownwards() {
 327         // If this node is not a leaf, we also put all
 328         // children into the same state as this branch
 329         if (! isLeaf()) {
 330             for (TreeItem<T> child : getChildren()) {
 331                 if (child instanceof CheckBoxTreeItem) {
 332                     CheckBoxTreeItem<T> cbti = ((CheckBoxTreeItem<T>) child);
 333                     cbti.setSelected(isSelected());
 334                 }
 335             }
 336         }
 337     }
 338 
 339     private void fireEvent(CheckBoxTreeItem<T> item, boolean selectionChanged) {
 340         Event evt = new CheckBoxTreeItem.TreeModificationEvent<T>(CHECK_BOX_SELECTION_CHANGED_EVENT, item, selectionChanged);
 341         Event.fireEvent(this, evt);
 342     }
 343 
 344 
 345     /**
 346      * A TreeModificationEvent class that works in a similar vein to the
 347      * {@link javafx.scene.control.TreeItem.TreeModificationEvent} class, in that
 348      * this event will bubble up the CheckBoxTreeItem hierarchy, until the parent
 349      * node is null.
 350      *
 351      * @param <T> The type of the value contained within the
 352      *      {@link CheckBoxTreeItem#valueProperty() value} property.
 353      * @since JavaFX 2.2
 354      */
 355     public static class TreeModificationEvent<T> extends Event {
 356         private static final long serialVersionUID = -8445355590698862999L;
 357 
 358         private transient final CheckBoxTreeItem<T> treeItem;
 359         private final boolean selectionChanged;
 360 
 361         /**
 362          * Common supertype for all tree modification event types.
 363          */
 364         public static final EventType<Event> ANY =
 365                 new EventType<Event> (Event.ANY, "TREE_MODIFICATION");
 366 
 367         /**
 368          * Creates a default TreeModificationEvent instance to represent the
 369          * change in selection/indeterminate states for the given CheckBoxTreeItem
 370          * instance.
 371          * @param eventType the eventType
 372          * @param treeItem the treeItem
 373          * @param selectionChanged the selectioonChanged
 374          */
 375         public TreeModificationEvent(EventType<? extends Event> eventType, CheckBoxTreeItem<T> treeItem, boolean selectionChanged) {
 376             super(eventType);
 377             this.treeItem = treeItem;
 378             this.selectionChanged = selectionChanged;
 379         }
 380 
 381         /**
 382          * Returns the CheckBoxTreeItem that this event occurred upon.
 383          * @return The CheckBoxTreeItem that this event occurred upon.
 384          */
 385         public CheckBoxTreeItem<T> getTreeItem() {
 386             return treeItem;
 387         }
 388 
 389         /**
 390          * Indicates the reason for this event is that the selection on the
 391          * CheckBoxTreeItem changed (as opposed to it becoming indeterminate).
 392          * @return has the CheckBoxTreeItem's selection changed
 393          */
 394         public boolean wasSelectionChanged() {
 395             return selectionChanged;
 396         }
 397 
 398         /**
 399          * Indicates the reason for this event is that the indeterminate
 400          * state on the CheckBoxTreeItem changed (as opposed to it becoming
 401          * selected or unselected).
 402          * @return has the CheckBoxTreeItem's indeterminate changed
 403          */
 404         public boolean wasIndeterminateChanged() {
 405             return ! selectionChanged;
 406         }
 407     }
 408 }