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<String> jonathanGiles = new CheckBoxTreeItem<String>("Jonathan"); 61 * CheckBoxTreeItem<String> juliaGiles = new CheckBoxTreeItem<String>("Julia"); 62 * CheckBoxTreeItem<String> mattGiles = new CheckBoxTreeItem<String>("Matt"); 63 * CheckBoxTreeItem<String> sueGiles = new CheckBoxTreeItem<String>("Sue"); 64 * CheckBoxTreeItem<String> ianGiles = new CheckBoxTreeItem<String>("Ian"); 65 * 66 * CheckBoxTreeItem<String> gilesFamily = new CheckBoxTreeItem<String>("Giles Family"); 67 * gilesFamily.setExpanded(true); 68 * gilesFamily.getChildren().addAll(jonathanGiles, juliaGiles, mattGiles, sueGiles, ianGiles); 69 * 70 * // create the treeView 71 * final TreeView<String> treeView = new TreeView<String>(); 72 * treeView.setRoot(gilesFamily); 73 * 74 * // set the cell factory 75 * treeView.setCellFactory(CheckBoxTreeCell.<String>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 }