1 /*
2  * Copyright 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.recyclerview.selection;
18 
19 import org.jspecify.annotations.NonNull;
20 import org.jspecify.annotations.Nullable;
21 
22 import java.util.Iterator;
23 import java.util.LinkedHashMap;
24 import java.util.LinkedHashSet;
25 import java.util.Map;
26 import java.util.Set;
27 
28 /**
29  * Object representing a "primary" selection and a "provisional" selection.
30  *
31  * <p>
32  * This class tracks selected items by managing two sets:
33  *
34  * <p>
35  * <b>Primary Selection</b>
36  *
37  * <p>
38  * Primary selection consists of items selected by a user. This represents the selection
39  * "at rest", as the selection does not contains items that are in a "provisional" selected
40  * state created by way of an ongoing gesture or band operation.
41  *
42  * <p>
43  * <b>Provisional Selection</b>
44  *
45  * <p>
46  * Provisional selections are selections which are interim in nature.
47  *
48  * <p>
49  * Provisional selection exists to address issues where a transitory selection might
50  * momentarily intersect with a previously established selection resulting in a some
51  * or all of the established selection being erased. Such situations may arise
52  * when band selection is being performed in "additive" mode (e.g. SHIFT or CTRL is pressed
53  * on the keyboard prior to mouse down), or when there's an active gesture selection
54  * (which can be initiated by long pressing an unselected item while there is an
55  * existing selection).
56  *
57  * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
58  * @see MutableSelection
59  */
60 public class Selection<K> implements Iterable<K> {
61 
62     // NOTE: Not currently private as DefaultSelectionTracker directly manipulates values.
63     final Set<K> mSelection;
64     final Set<K> mProvisionalSelection;
65 
Selection()66     Selection() {
67         mSelection = new LinkedHashSet<>();
68         mProvisionalSelection = new LinkedHashSet<>();
69     }
70 
71     /**
72      * Used by {@link StorageStrategy} when restoring selection.
73      */
Selection(@onNull Set<K> selection)74     Selection(@NonNull Set<K> selection) {
75         mSelection = selection;
76         mProvisionalSelection = new LinkedHashSet<>();
77     }
78 
79     /**
80      * @return true if the position is currently selected.
81      */
contains(@ullable K key)82     public boolean contains(@Nullable K key) {
83         return mSelection.contains(key) || mProvisionalSelection.contains(key);
84     }
85 
86     /**
87      * Returns an {@link Iterator} that iterators over the selection, *excluding*
88      * any provisional selection.
89      *
90      * {@inheritDoc}
91      */
92     @Override
iterator()93     public @NonNull Iterator<K> iterator() {
94         return mSelection.iterator();
95     }
96 
97     /**
98      * @return size of the selection including both final and provisional selected items.
99      */
size()100     public int size() {
101         return mSelection.size() + mProvisionalSelection.size();
102     }
103 
104     /**
105      * @return true if the selection is empty.
106      */
isEmpty()107     public boolean isEmpty() {
108         return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
109     }
110 
111     /**
112      * Sets the provisional selection, which is a temporary selection that can be saved,
113      * canceled, or adjusted at a later time. When a new provision selection is applied, the old
114      * one (if it exists) is abandoned.
115      *
116      * @return Map of ids added or removed. Added ids have a value of true, removed are false.
117      */
setProvisionalSelection(@onNull Set<K> newSelection)118     Map<K, Boolean> setProvisionalSelection(@NonNull Set<K> newSelection) {
119         Map<K, Boolean> delta = new LinkedHashMap<>();
120 
121         for (K key : mProvisionalSelection) {
122             // Mark each item that used to be in the provisional selection
123             // but is not in the new provisional selection.
124             if (!newSelection.contains(key) && !mSelection.contains(key)) {
125                 delta.put(key, false);
126             }
127         }
128 
129         for (K key : mSelection) {
130             // Mark each item that in the selection but is not in the new
131             // provisional selection.
132             if (!newSelection.contains(key)) {
133                 delta.put(key, false);
134             }
135         }
136 
137         for (K key : newSelection) {
138             // Mark each item that was not previously in the selection but is in the new
139             // provisional selection.
140             if (!mSelection.contains(key) && !mProvisionalSelection.contains(key)) {
141                 delta.put(key, true);
142             }
143         }
144 
145         // Now, iterate through the changes and actually add/remove them to/from the current
146         // selection. This could not be done in the previous loops because changing the size of
147         // the selection mid-iteration changes iteration order erroneously.
148         for (Map.Entry<K, Boolean> entry : delta.entrySet()) {
149             K key = entry.getKey();
150             if (entry.getValue()) {
151                 mProvisionalSelection.add(key);
152             } else {
153                 mProvisionalSelection.remove(key);
154             }
155         }
156 
157         return delta;
158     }
159 
160     /**
161      * Saves the existing provisional selection. Once the provisional selection is saved,
162      * subsequent provisional selections which are different from this existing one cannot
163      * cause items in this existing provisional selection to become deselected.
164      */
mergeProvisionalSelection()165     void mergeProvisionalSelection() {
166         mSelection.addAll(mProvisionalSelection);
167         mProvisionalSelection.clear();
168     }
169 
170     /**
171      * Abandons the existing provisional selection so that all items provisionally selected are
172      * now deselected.
173      */
clearProvisionalSelection()174     void clearProvisionalSelection() {
175         mProvisionalSelection.clear();
176     }
177 
178     /**
179      * Adds a new item to the primary selection.
180      *
181      * @return true if the operation resulted in a modification to the selection.
182      */
add(@onNull K key)183     boolean add(@NonNull K key) {
184         return mSelection.add(key);
185     }
186 
187     /**
188      * Removes an item from the primary selection.
189      *
190      * @return true if the operation resulted in a modification to the selection.
191      */
remove(@onNull K key)192     boolean remove(@NonNull K key) {
193         return mSelection.remove(key);
194     }
195 
196     /**
197      * Clears the primary selection. The provisional selection, if any, is unaffected.
198      */
clear()199     void clear() {
200         mSelection.clear();
201     }
202 
203     /**
204      * Clones primary and provisional selection from supplied {@link Selection}.
205      * Does not copy active range data.
206      */
copyFrom(@onNull Selection<K> source)207     void copyFrom(@NonNull Selection<K> source) {
208         mSelection.clear();
209         mSelection.addAll(source.mSelection);
210 
211         mProvisionalSelection.clear();
212         mProvisionalSelection.addAll(source.mProvisionalSelection);
213     }
214 
215     @Override
toString()216     public String toString() {
217         if (size() <= 0) {
218             return "size=0, items=[]";
219         }
220 
221         StringBuilder buffer = new StringBuilder(size() * 28);
222         buffer.append("Selection{")
223                 .append("primary{size=" + mSelection.size())
224                 .append(", entries=" + mSelection)
225                 .append("}, provisional{size=" + mProvisionalSelection.size())
226                 .append(", entries=" + mProvisionalSelection)
227                 .append("}}");
228         return buffer.toString();
229     }
230 
231     @Override
hashCode()232     public int hashCode() {
233         return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
234     }
235 
236     @Override
equals(Object other)237     public boolean equals(Object other) {
238         return (this == other)
239                 || (other instanceof Selection && isEqualTo((Selection<?>) other));
240     }
241 
isEqualTo(Selection<?> other)242     private boolean isEqualTo(Selection<?> other) {
243         return mSelection.equals(other.mSelection)
244                 && mProvisionalSelection.equals(other.mProvisionalSelection);
245     }
246 }
247