1 2 package com.badlogic.gdx.scenes.scene2d.utils; 3 4 import com.badlogic.gdx.scenes.scene2d.Actor; 5 import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent; 6 import com.badlogic.gdx.utils.Array; 7 import com.badlogic.gdx.utils.OrderedSet; 8 import com.badlogic.gdx.utils.Pools; 9 10 import java.util.Iterator; 11 12 /** Manages selected objects. Optionally fires a {@link ChangeEvent} on an actor. Selection changes can be vetoed via 13 * {@link ChangeEvent#cancel()}. 14 * @author Nathan Sweet */ 15 public class Selection<T> implements Disableable, Iterable<T> { 16 private Actor actor; 17 final OrderedSet<T> selected = new OrderedSet(); 18 private final OrderedSet<T> old = new OrderedSet(); 19 boolean isDisabled; 20 private boolean toggle; 21 boolean multiple; 22 boolean required; 23 private boolean programmaticChangeEvents = true; 24 T lastSelected; 25 26 /** @param actor An actor to fire {@link ChangeEvent} on when the selection changes, or null. */ 27 public void setActor (Actor actor) { 28 this.actor = actor; 29 } 30 31 /** Selects or deselects the specified item based on how the selection is configured, whether ctrl is currently pressed, etc. 32 * This is typically invoked by user interaction. */ 33 public void choose (T item) { 34 if (item == null) throw new IllegalArgumentException("item cannot be null."); 35 if (isDisabled) return; 36 snapshot(); 37 try { 38 if ((toggle || (!required && selected.size == 1) || UIUtils.ctrl()) && selected.contains(item)) { 39 if (required && selected.size == 1) return; 40 selected.remove(item); 41 lastSelected = null; 42 } else { 43 boolean modified = false; 44 if (!multiple || (!toggle && !UIUtils.ctrl())) { 45 if (selected.size == 1 && selected.contains(item)) return; 46 modified = selected.size > 0; 47 selected.clear(); 48 } 49 if (!selected.add(item) && !modified) return; 50 lastSelected = item; 51 } 52 if (fireChangeEvent()) revert(); 53 } finally { 54 cleanup(); 55 } 56 } 57 58 public boolean hasItems () { 59 return selected.size > 0; 60 } 61 62 public boolean isEmpty () { 63 return selected.size == 0; 64 } 65 66 public int size () { 67 return selected.size; 68 } 69 70 public OrderedSet<T> items () { 71 return selected; 72 } 73 74 /** Returns the first selected item, or null. */ 75 public T first () { 76 return selected.size == 0 ? null : selected.first(); 77 } 78 79 void snapshot () { 80 old.clear(); 81 old.addAll(selected); 82 } 83 84 void revert () { 85 selected.clear(); 86 selected.addAll(old); 87 } 88 89 void cleanup () { 90 old.clear(32); 91 } 92 93 /** Sets the selection to only the specified item. */ 94 public void set (T item) { 95 if (item == null) throw new IllegalArgumentException("item cannot be null."); 96 if (selected.size == 1 && selected.first() == item) return; 97 snapshot(); 98 selected.clear(); 99 selected.add(item); 100 if (programmaticChangeEvents && fireChangeEvent()) 101 revert(); 102 else 103 lastSelected = item; 104 cleanup(); 105 } 106 107 public void setAll (Array<T> items) { 108 boolean added = false; 109 snapshot(); 110 selected.clear(); 111 for (int i = 0, n = items.size; i < n; i++) { 112 T item = items.get(i); 113 if (item == null) throw new IllegalArgumentException("item cannot be null."); 114 if (selected.add(item)) added = true; 115 } 116 if (added && programmaticChangeEvents && fireChangeEvent()) 117 revert(); 118 else 119 lastSelected = items.peek(); 120 cleanup(); 121 } 122 123 /** Adds the item to the selection. */ 124 public void add (T item) { 125 if (item == null) throw new IllegalArgumentException("item cannot be null."); 126 if (!selected.add(item)) return; 127 if (programmaticChangeEvents && fireChangeEvent()) 128 selected.remove(item); 129 else 130 lastSelected = item; 131 } 132 133 public void addAll (Array<T> items) { 134 boolean added = false; 135 snapshot(); 136 for (int i = 0, n = items.size; i < n; i++) { 137 T item = items.get(i); 138 if (item == null) throw new IllegalArgumentException("item cannot be null."); 139 if (selected.add(item)) added = true; 140 } 141 if (added && programmaticChangeEvents && fireChangeEvent()) 142 revert(); 143 else 144 lastSelected = items.peek(); 145 cleanup(); 146 } 147 148 public void remove (T item) { 149 if (item == null) throw new IllegalArgumentException("item cannot be null."); 150 if (!selected.remove(item)) return; 151 if (programmaticChangeEvents && fireChangeEvent()) 152 selected.add(item); 153 else 154 lastSelected = null; 155 } 156 157 public void removeAll (Array<T> items) { 158 boolean removed = false; 159 snapshot(); 160 for (int i = 0, n = items.size; i < n; i++) { 161 T item = items.get(i); 162 if (item == null) throw new IllegalArgumentException("item cannot be null."); 163 if (selected.remove(item)) removed = true; 164 } 165 if (removed && programmaticChangeEvents && fireChangeEvent()) 166 revert(); 167 else 168 lastSelected = null; 169 cleanup(); 170 } 171 172 public void clear () { 173 if (selected.size == 0) return; 174 snapshot(); 175 selected.clear(); 176 if (programmaticChangeEvents && fireChangeEvent()) 177 revert(); 178 else 179 lastSelected = null; 180 cleanup(); 181 } 182 183 /** Fires a change event on the selection's actor, if any. Called internally when the selection changes, depending on 184 * {@link #setProgrammaticChangeEvents(boolean)}. 185 * @return true if the change should be undone. */ 186 public boolean fireChangeEvent () { 187 if (actor == null) return false; 188 ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class); 189 try { 190 return actor.fire(changeEvent); 191 } finally { 192 Pools.free(changeEvent); 193 } 194 } 195 196 public boolean contains (T item) { 197 if (item == null) return false; 198 return selected.contains(item); 199 } 200 201 /** Makes a best effort to return the last item selected, else returns an arbitrary item or null if the selection is empty. */ 202 public T getLastSelected () { 203 if (lastSelected != null) { 204 return lastSelected; 205 } else if (selected.size > 0) { 206 return selected.first(); 207 } 208 return null; 209 } 210 211 public Iterator<T> iterator () { 212 return selected.iterator(); 213 } 214 215 public Array<T> toArray () { 216 return selected.iterator().toArray(); 217 } 218 219 public Array<T> toArray (Array<T> array) { 220 return selected.iterator().toArray(array); 221 } 222 223 /** If true, prevents {@link #choose(Object)} from changing the selection. Default is false. */ 224 public void setDisabled (boolean isDisabled) { 225 this.isDisabled = isDisabled; 226 } 227 228 public boolean isDisabled () { 229 return isDisabled; 230 } 231 232 public boolean getToggle () { 233 return toggle; 234 } 235 236 /** If true, prevents {@link #choose(Object)} from clearing the selection. Default is false. */ 237 public void setToggle (boolean toggle) { 238 this.toggle = toggle; 239 } 240 241 public boolean getMultiple () { 242 return multiple; 243 } 244 245 /** If true, allows {@link #choose(Object)} to select multiple items. Default is false. */ 246 public void setMultiple (boolean multiple) { 247 this.multiple = multiple; 248 } 249 250 public boolean getRequired () { 251 return required; 252 } 253 254 /** If true, prevents {@link #choose(Object)} from selecting none. Default is false. */ 255 public void setRequired (boolean required) { 256 this.required = required; 257 } 258 259 /** If false, only {@link #choose(Object)} will fire a change event. Default is true. */ 260 public void setProgrammaticChangeEvents (boolean programmaticChangeEvents) { 261 this.programmaticChangeEvents = programmaticChangeEvents; 262 } 263 264 public String toString () { 265 return selected.toString(); 266 } 267 } 268