Web Inspector: REGRESSION(r238599): Multiple Selection: restoring selection when...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Controllers / SelectionController.js
1 /*
2  * Copyright (C) 2018 Apple Inc. All Rights Reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.SelectionController = class SelectionController extends WI.Object
27 {
28     constructor(delegate)
29     {
30         super();
31
32         console.assert(delegate);
33         this._delegate = delegate;
34
35         this._allowsEmptySelection = true;
36         this._allowsMultipleSelection = false;
37         this._lastSelectedIndex = NaN;
38         this._shiftAnchorIndex = NaN;
39         this._selectedIndexes = new WI.IndexSet;
40
41         console.assert(this._delegate.selectionControllerNumberOfItems, "SelectionController delegate must implement selectionControllerNumberOfItems.");
42         console.assert(this._delegate.selectionControllerNextSelectableIndex, "SelectionController delegate must implement selectionControllerNextSelectableIndex.");
43         console.assert(this._delegate.selectionControllerPreviousSelectableIndex, "SelectionController delegate must implement selectionControllerPreviousSelectableIndex.");
44     }
45
46     // Public
47
48     get delegate() { return this._delegate; }
49     get lastSelectedItem() { return this._lastSelectedIndex; }
50     get selectedItems() { return this._selectedIndexes; }
51
52     get allowsEmptySelection() { return this._allowsEmptySelection; }
53     set allowsEmptySelection(flag) { this._allowsEmptySelection = flag; }
54
55     get allowsMultipleSelection()
56     {
57         return this._allowsMultipleSelection;
58     }
59
60     set allowsMultipleSelection(flag)
61     {
62         if (this._allowsMultipleSelection === flag)
63             return;
64
65         this._allowsMultipleSelection = flag;
66         if (this._allowsMultipleSelection)
67             return;
68
69         if (this._selectedIndexes.size > 1) {
70             console.assert(this._lastSelectedIndex >= 0);
71             this._updateSelectedItems(new WI.IndexSet([this._lastSelectedIndex]));
72         }
73     }
74
75     get numberOfItems()
76     {
77         return this._delegate.selectionControllerNumberOfItems(this);
78     }
79
80     hasSelectedItem(index)
81     {
82         return this._selectedIndexes.has(index);
83     }
84
85     selectItem(index, extendSelection = false)
86     {
87         console.assert(!extendSelection || this._allowsMultipleSelection, "Cannot extend selection with multiple selection disabled.");
88         console.assert(index >= 0 && index < this.numberOfItems);
89
90         if (this.hasSelectedItem(index)) {
91             if (!extendSelection)
92                 this._deselectAllAndSelect(index);
93             return;
94         }
95
96         let newSelectedItems = extendSelection ? this._selectedIndexes.copy() : new WI.IndexSet;
97         newSelectedItems.add(index);
98
99         this._shiftAnchorIndex = NaN;
100         this._lastSelectedIndex = index;
101
102         this._updateSelectedItems(newSelectedItems);
103     }
104
105     deselectItem(index)
106     {
107         console.assert(index >= 0 && index < this.numberOfItems);
108
109         if (!this.hasSelectedItem(index))
110             return;
111
112         if (!this._allowsEmptySelection && this._selectedIndexes.size === 1)
113             return;
114
115         let newSelectedItems = this._selectedIndexes.copy();
116         newSelectedItems.delete(index);
117
118         if (this._shiftAnchorIndex === index)
119             this._shiftAnchorIndex = NaN;
120
121         if (this._lastSelectedIndex === index) {
122             this._lastSelectedIndex = NaN;
123             if (newSelectedItems.size) {
124                 // Find selected item closest to deselected item.
125                 let preceding = newSelectedItems.indexLessThan(index);
126                 let following = newSelectedItems.indexGreaterThan(index);
127
128                 if (isNaN(preceding))
129                     this._lastSelectedIndex = following;
130                 else if (isNaN(following))
131                     this._lastSelectedIndex = preceding;
132                 else {
133                     if ((following - index) < (index - preceding))
134                         this._lastSelectedIndex = following;
135                     else
136                         this._lastSelectedIndex = preceding;
137                 }
138             }
139         }
140
141         this._updateSelectedItems(newSelectedItems);
142     }
143
144     selectAll()
145     {
146         if (!this.numberOfItems || !this._allowsMultipleSelection)
147             return;
148
149         if (this._selectedIndexes.size === this.numberOfItems)
150             return;
151
152         let newSelectedItems = new WI.IndexSet;
153         newSelectedItems.addRange(0, this.numberOfItems);
154
155         this._lastSelectedIndex = newSelectedItems.lastIndex;
156         if (isNaN(this._shiftAnchorIndex))
157             this._shiftAnchorIndex = this._lastSelectedIndex;
158
159         this._updateSelectedItems(newSelectedItems);
160     }
161
162     deselectAll()
163     {
164         const index = NaN;
165         this._deselectAllAndSelect(index);
166     }
167
168     removeSelectedItems()
169     {
170         let numberOfSelectedItems = this._selectedIndexes.size;
171         if (!numberOfSelectedItems)
172             return;
173
174         // Try selecting the item following the selection.
175         let lastSelectedIndex = this._selectedIndexes.lastIndex;
176         let indexToSelect = lastSelectedIndex + 1;
177         if (indexToSelect === this.numberOfItems) {
178             // If no item exists after the last item in the selection, try selecting
179             // a deselected item (hole) within the selection.
180             let firstSelectedIndex = this._selectedIndexes.firstIndex;
181             if (lastSelectedIndex - firstSelectedIndex > numberOfSelectedItems) {
182                 indexToSelect = this._selectedIndexes.firstIndex + 1;
183                 while (this._selectedIndexes.has(indexToSelect))
184                     indexToSelect++;
185             } else {
186                 // If the selection contains no holes, try selecting the item
187                 // preceding the selection.
188                 indexToSelect = firstSelectedIndex > 0 ? firstSelectedIndex - 1 : NaN;
189             }
190         }
191
192         this._deselectAllAndSelect(indexToSelect);
193     }
194
195     reset()
196     {
197         this._shiftAnchorIndex = NaN;
198         this._lastSelectedIndex = NaN;
199         this._selectedIndexes.clear();
200     }
201
202     didInsertItem(index)
203     {
204         let current = this._selectedIndexes.lastIndex;
205         while (current >= index) {
206             this._selectedIndexes.delete(current);
207             this._selectedIndexes.add(current + 1);
208
209             current = this._selectedIndexes.indexLessThan(current);
210         }
211     }
212
213     didRemoveItem(index)
214     {
215         if (this.hasSelectedItem(index))
216             this.deselectItem(index);
217
218         while (index = this._selectedIndexes.indexGreaterThan(index)) {
219             this._selectedIndexes.delete(index);
220             this._selectedIndexes.add(index - 1);
221         }
222     }
223
224     handleKeyDown(event)
225     {
226         if (!this.numberOfItems)
227             return false;
228
229         if (event.key === "a" && event.commandOrControlKey) {
230             this.selectAll();
231             return true;
232         }
233
234         if (event.metaKey || event.ctrlKey)
235             return false;
236
237         if (event.keyIdentifier === "Up" || event.keyIdentifier === "Down") {
238             this._selectItemsFromArrowKey(event.keyIdentifier === "Up", event.shiftKey);
239
240             event.preventDefault();
241             event.stopPropagation();
242             return true;
243         }
244
245         return false;
246     }
247
248     handleItemMouseDown(index, event)
249     {
250         if (event.button !== 0 || event.ctrlKey)
251             return;
252
253         // Command (macOS) or Control (Windows) key takes precedence over shift
254         // whether or not multiple selection is enabled, so handle it first.
255         if (event.commandOrControlKey) {
256             if (this.hasSelectedItem(index))
257                 this.deselectItem(index);
258             else
259                 this.selectItem(index, this._allowsMultipleSelection);
260             return;
261         }
262
263         let shiftExtendSelection = this._allowsMultipleSelection && event.shiftKey;
264         if (!shiftExtendSelection) {
265             this.selectItem(index);
266             return;
267         }
268
269         let newSelectedItems = this._selectedIndexes.copy();
270
271         // Shift-clicking when nothing is selected should cause the first item
272         // through the clicked item to be selected.
273         if (!newSelectedItems.size) {
274             this._shiftAnchorIndex = 0;
275             this._lastSelectedIndex = index;
276             newSelectedItems.addRange(0, index + 1);
277             this._updateSelectedItems(newSelectedItems);
278             return;
279         }
280
281         if (isNaN(this._shiftAnchorIndex))
282             this._shiftAnchorIndex = this._lastSelectedIndex;
283
284         // Shift-clicking will add to or delete from the current selection, or
285         // pivot the selection around the anchor (a delete followed by an add).
286         // We could check for all three cases, and add or delete only those items
287         // that are necessary, but it is simpler to throw out the previous shift-
288         // selected range and add the new range between the anchor and clicked item.
289
290         function normalizeRange(startIndex, endIndex) {
291             return startIndex > endIndex ? [endIndex, startIndex] : [startIndex, endIndex];
292         }
293
294         if (this._shiftAnchorIndex !== this._lastSelectedIndex) {
295             let [startIndex, endIndex] = normalizeRange(this._shiftAnchorIndex, this._lastSelectedIndex);
296             newSelectedItems.deleteRange(startIndex, endIndex - startIndex + 1);
297         }
298
299         let [startIndex, endIndex] = normalizeRange(this._shiftAnchorIndex, index);
300         newSelectedItems.addRange(startIndex, endIndex - startIndex + 1);
301
302         this._lastSelectedIndex = index;
303
304         this._updateSelectedItems(newSelectedItems);
305     }
306
307     // Private
308
309     _deselectAllAndSelect(index)
310     {
311         if (!this._selectedIndexes.size)
312             return;
313
314         if (this._selectedIndexes.size === 1 && this._selectedIndexes.firstIndex === index)
315             return;
316
317         this._shiftAnchorIndex = NaN;
318         this._lastSelectedIndex = index;
319
320         let newSelectedItems = new WI.IndexSet;
321         if (!isNaN(index))
322             newSelectedItems.add(index);
323
324         this._updateSelectedItems(newSelectedItems);
325     }
326
327     _selectItemsFromArrowKey(goingUp, shiftKey)
328     {
329         if (!this._selectedIndexes.size) {
330             let index = goingUp ? this.numberOfItems - 1 : 0;
331             this.selectItem(index);
332             return;
333         }
334
335         let index = goingUp ? this._previousSelectableIndex(this._lastSelectedIndex) : this._nextSelectableIndex(this._lastSelectedIndex);
336         if (isNaN(index))
337             return;
338
339         let extendSelection = shiftKey && this._allowsMultipleSelection;
340         if (!extendSelection || !this.hasSelectedItem(index)) {
341             this.selectItem(index, extendSelection);
342             return;
343         }
344
345         // Since the item in the direction of movement is selected, we are either
346         // extending the selection into the item, or deselecting. Determine which
347         // by checking whether the item opposite the anchor item is selected.
348         let priorIndex = goingUp ? this._nextSelectableIndex(this._lastSelectedIndex) : this._previousSelectableIndex(this._lastSelectedIndex);
349         if (!this.hasSelectedItem(priorIndex)) {
350             this.deselectItem(this._lastSelectedIndex);
351             return;
352         }
353
354         // The selection is being extended into the item; make it the new
355         // anchor item then continue searching in the direction of movement
356         // for an unselected item to select.
357         while (!isNaN(index)) {
358             if (!this.hasSelectedItem(index)) {
359                 this.selectItem(index, extendSelection);
360                 break;
361             }
362
363             this._lastSelectedIndex = index;
364             index = goingUp ? this._previousSelectableIndex(index) : this._nextSelectableIndex(index);
365         }
366     }
367
368     _nextSelectableIndex(index)
369     {
370         return this._delegate.selectionControllerNextSelectableIndex(this, index);
371     }
372
373     _previousSelectableIndex(index)
374     {
375         return this._delegate.selectionControllerPreviousSelectableIndex(this, index);
376     }
377
378     _updateSelectedItems(indexes)
379     {
380         if (this._selectedIndexes.equals(indexes))
381             return;
382
383         let oldSelectedIndexes = this._selectedIndexes.copy();
384         this._selectedIndexes = indexes;
385
386         if (!this._delegate.selectionControllerSelectionDidChange)
387             return;
388
389         let deselectedItems = oldSelectedIndexes.difference(indexes);
390         let selectedItems = indexes.difference(oldSelectedIndexes);
391         this._delegate.selectionControllerSelectionDidChange(this, deselectedItems, selectedItems);
392     }
393 };