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  * @see CheckBoxTreeCell
  78  * @see TreeItem
  79  * @see CheckBox
  80  * @since JavaFX 2.2
  81  */
  82 // TODO the TreeModificationEvent doesn't actually bubble in the same way as
  83 // TreeItem - it just looks that way as the 'bubbling' is done via changing the
  84 // state of all parent items.
  85 public class CheckBoxTreeItem<T> extends TreeItem<T> {
  86 
  87     /**
  88      * An EventType used when the CheckBoxTreeItem selection / indeterminate
  89      * state changes. To use this, it is recommended that you use code along the
  90      * lines of the following:
  91      *
  92      *<pre>
  93      * {@code
  94      * child1.addEventHandler(CheckBoxTreeItem.<String>checkBoxSelectionChangedEvent(), new EventHandler<TreeModificationEvent<String>>() {
  95      *     public void handle(TreeModificationEvent<String> event) {
  96      *          ...
  97      *     }
  98      * });}
  99      * </pre>
 100      *
 101      * @param <T> The type of the value contained within the TreeItem.
 102      */
 103     @SuppressWarnings("unchecked")
 104     public static <T> EventType<TreeModificationEvent<T>> checkBoxSelectionChangedEvent() {
 105         return (EventType<TreeModificationEvent<T>>) CHECK_BOX_SELECTION_CHANGED_EVENT;
 106     }
 107     private static final EventType<? extends Event> CHECK_BOX_SELECTION_CHANGED_EVENT
 108             = new EventType<Event>(TreeModificationEvent.ANY, "checkBoxSelectionChangedEvent");
 109 
 110     /***************************************************************************
 111      *                                                                         *
 112      * Constructors                                                            *
 113      *                                                                         *
 114      **************************************************************************/
 115 
 116     /**
 117      * Creates an empty CheckBoxTreeItem.
 118      */
 119     public CheckBoxTreeItem() {
 120         this(null);
 121     }
 122 
 123     /**
 124      * Creates a CheckBoxTreeItem with the value property set to the provided
 125      * object.
 126      *
 127      * @param value The object to be stored as the value of this TreeItem.
 128      */
 129     public CheckBoxTreeItem(T value) {
 130         this(value, null, false);
 131     }
 132 
 133     /**
 134      * Creates a CheckBoxTreeItem with the value property set to the provided
 135      * object, and the graphic set to the provided Node.
 136      *
 137      * @param value The object to be stored as the value of this CheckBoxTreeItem.
 138      * @param graphic The Node to show in the TreeView next to this CheckBoxTreeItem.
 139      */
 140     public CheckBoxTreeItem(T value, Node graphic) {
 141         this(value, graphic, false);
 142     }
 143 
 144     /**
 145      * Creates a CheckBoxTreeItem with the value property set to the provided
 146      * object, the graphic set to the provided Node, and the initial state
 147      * of the {@link #selectedProperty()} set to the provided boolean value.
 148      *
 149      * @param value The object to be stored as the value of this CheckBoxTreeItem.
 150      * @param graphic The Node to show in the TreeView next to this CheckBoxTreeItem.
 151      * @param selected The initial value of the
 152      *            {@link #selectedProperty() selected} property.
 153      */
 154     public CheckBoxTreeItem(T value, Node graphic, boolean selected) {
 155         this(value, graphic, selected, false);
 156     }
 157 
 158     /**
 159      * Creates a CheckBoxTreeItem with the value property set to the provided
 160      * object, the graphic set to the provided Node, the initial state
 161      * of the {@link #selectedProperty()} set to the provided boolean value, and
 162      * the initial state of the {@link #independentProperty() independent}
 163      * property to the provided boolean value.
 164      *
 165      * @param value The object to be stored as the value of this CheckBoxTreeItem.
 166      * @param graphic The Node to show in the TreeView next to this CheckBoxTreeItem.
 167      * @param selected The initial value of the
 168      *            {@link #selectedProperty() selected} property.
 169      * @param independent The initial value of the
 170      *            {@link #independentProperty() independent} property
 171      */
 172     public CheckBoxTreeItem(T value, Node graphic, boolean selected, boolean independent) {
 173         super(value, graphic);
 174         setSelected(selected);
 175         setIndependent(independent);
 176 
 177         selectedProperty().addListener(stateChangeListener);
 178         indeterminateProperty().addListener(stateChangeListener);
 179     }
 180 
 181 
 182 
 183     /***************************************************************************
 184      *                                                                         *
 185      * Callbacks                                                               *
 186      *                                                                         *
 187      **************************************************************************/
 188     private final ChangeListener<Boolean> stateChangeListener = (ov, oldVal, newVal) -> {
 189         updateState();
 190     };
 191 
 192 
 193     /***************************************************************************
 194      *                                                                         *
 195      * Properties                                                              *
 196      *                                                                         *
 197      **************************************************************************/
 198 
 199     // --- Selected
 200     private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected", false) {
 201         @Override protected void invalidated() {
 202             super.invalidated();
 203             fireEvent(CheckBoxTreeItem.this, true);
 204         }
 205     };
 206     /** Sets the selected state of this CheckBoxTreeItem. */
 207     public final void setSelected(boolean value) { selectedProperty().setValue(value); }
 208     /** Returns the selected state of this CheckBoxTreeItem. */
 209     public final boolean isSelected() { return selected.getValue(); }
 210     /** A {@link BooleanProperty} used to represent the selected state of this CheckBoxTreeItem. */
 211     public final BooleanProperty selectedProperty() { return selected; }
 212 
 213 
 214     // --- Indeterminate
 215     private final BooleanProperty indeterminate = new SimpleBooleanProperty(this, "indeterminate", false) {
 216         @Override protected void invalidated() {
 217             super.invalidated();
 218             fireEvent(CheckBoxTreeItem.this, false);
 219         }
 220     };
 221     /** Sets the indeterminate state of this CheckBoxTreeItem. */
 222     public final void setIndeterminate(boolean value) { indeterminateProperty().setValue(value); }
 223     /** Returns the indeterminate state of this CheckBoxTreeItem. */
 224     public final boolean isIndeterminate() { return indeterminate.getValue(); }
 225     /** A {@link BooleanProperty} used to represent the indeterminate state of this CheckBoxTreeItem. */
 226     public final BooleanProperty indeterminateProperty() { return indeterminate; }
 227 
 228 
 229     // --- Independent
 230     /**
 231      * A {@link BooleanProperty} used to represent the independent state of this CheckBoxTreeItem.
 232      * The independent state is used to represent whether changes to a single
 233      * CheckBoxTreeItem should influence the state of its parent and children.
 234      *
 235      * <p>By default, the independent property is false, which means that when
 236      * a CheckBoxTreeItem has state changes to the selected or indeterminate
 237      * properties, the state of related CheckBoxTreeItems will possibly be changed.
 238      * If the independent property is set to true, the state of related CheckBoxTreeItems
 239      * will <b>never</b> change.
 240      */
 241     public final BooleanProperty independentProperty() { return independent; }
 242     private final BooleanProperty independent = new SimpleBooleanProperty(this, "independent", false);
 243     public final void setIndependent(boolean value) { independentProperty().setValue(value); }
 244     public final boolean isIndependent() { return independent.getValue(); }
 245 
 246 
 247 
 248     /***************************************************************************
 249      *                                                                         *
 250      * Private Implementation                                                  *
 251      *                                                                         *
 252      **************************************************************************/
 253 
 254     private static boolean updateLock = false;
 255 
 256     private void updateState() {
 257         if (isIndependent()) return;
 258 
 259         boolean firstLock = ! updateLock;
 260 
 261         // toggle parent (recursively up to root)
 262         updateLock = true;
 263         updateUpwards();
 264 
 265         if (firstLock) updateLock = false;
 266 
 267         // toggle children
 268         if (updateLock) return;
 269         updateDownwards();
 270     }
 271 
 272     private void updateUpwards() {
 273         if (! (getParent() instanceof CheckBoxTreeItem)) return;
 274 
 275         CheckBoxTreeItem<?> parent = (CheckBoxTreeItem<?>) getParent();
 276         int selectCount = 0;
 277         int indeterminateCount = 0;
 278         for (TreeItem<?> child : parent.getChildren()) {
 279             if (! (child instanceof CheckBoxTreeItem)) continue;
 280 
 281             CheckBoxTreeItem<?> cbti = (CheckBoxTreeItem<?>) child;
 282 
 283             selectCount += cbti.isSelected() && ! cbti.isIndeterminate() ? 1 : 0;
 284             indeterminateCount += cbti.isIndeterminate() ? 1 : 0;
 285         }
 286 
 287         if (selectCount == parent.getChildren().size()) {
 288             parent.setSelected(true);
 289             parent.setIndeterminate(false);
 290         } else if (selectCount == 0 && indeterminateCount == 0) {
 291             parent.setSelected(false);
 292             parent.setIndeterminate(false);
 293         } else {
 294             parent.setIndeterminate(true);
 295         }
 296     }
 297 
 298     private void updateDownwards() {
 299         // If this node is not a leaf, we also put all
 300         // children into the same state as this branch
 301         if (! isLeaf()) {
 302             for (TreeItem<T> child : getChildren()) {
 303                 if (child instanceof CheckBoxTreeItem) {
 304                     CheckBoxTreeItem<T> cbti = ((CheckBoxTreeItem<T>) child);
 305                     cbti.setSelected(isSelected());
 306                 }
 307             }
 308         }
 309     }
 310 
 311     private void fireEvent(CheckBoxTreeItem<T> item, boolean selectionChanged) {
 312         Event evt = new CheckBoxTreeItem.TreeModificationEvent<T>(CHECK_BOX_SELECTION_CHANGED_EVENT, item, selectionChanged);
 313         Event.fireEvent(this, evt);
 314     }
 315 
 316 
 317     /**
 318      * A TreeModificationEvent class that works in a similar vein to the
 319      * {@link javafx.scene.control.TreeItem.TreeModificationEvent} class, in that
 320      * this event will bubble up the CheckBoxTreeItem hierarchy, until the parent
 321      * node is null.
 322      *
 323      * @param <T> The type of the value contained within the
 324      *      {@link CheckBoxTreeItem#valueProperty() value} property.
 325      * @since JavaFX 2.2
 326      */
 327     public static class TreeModificationEvent<T> extends Event {
 328         private static final long serialVersionUID = -8445355590698862999L;
 329 
 330         private transient final CheckBoxTreeItem<T> treeItem;
 331         private final boolean selectionChanged;
 332 
 333         /**
 334          * Common supertype for all tree modification event types.
 335          */
 336         public static final EventType<Event> ANY =
 337                 new EventType<Event> (Event.ANY, "TREE_MODIFICATION");
 338 
 339         /**
 340          * Creates a default TreeModificationEvent instance to represent the
 341          * change in selection/indeterminate states for the given CheckBoxTreeItem
 342          * instance.
 343          */
 344         public TreeModificationEvent(EventType<? extends Event> eventType, CheckBoxTreeItem<T> treeItem, boolean selectionChanged) {
 345             super(eventType);
 346             this.treeItem = treeItem;
 347             this.selectionChanged = selectionChanged;
 348         }
 349 
 350         /**
 351          * Returns the CheckBoxTreeItem that this event occurred upon.
 352          * @return The CheckBoxTreeItem that this event occurred upon.
 353          */
 354         public CheckBoxTreeItem<T> getTreeItem() {
 355             return treeItem;
 356         }
 357 
 358         /**
 359          * Indicates the the reason for this event is that the selection on the
 360          * CheckBoxTreeItem changed (as opposed to it becoming indeterminate).
 361          */
 362         public boolean wasSelectionChanged() {
 363             return selectionChanged;
 364         }
 365 
 366         /**
 367          * Indicates the the reason for this event is that the indeterminate
 368          * state on the CheckBoxTreeItem changed (as opposed to it becoming
 369          * selected or unselected).
 370          */
 371         public boolean wasIndeterminateChanged() {
 372             return ! selectionChanged;
 373         }
 374     }
 375 }