jpnurmi / item_selector
1
// MIT License
2
//
3
// Copyright (c) 2020 J-P Nurmi
4
//
5
// The ItemSelector library is based on:
6
// Multi Select GridView in Flutter - by Simon Lightfoot:
7
// https://gist.github.com/slightfoot/a002dd1e031f5f012f810c6d5da14a11
8
//
9
// Copyright (c) 2019 Simon Lightfoot
10
//
11
// Permission is hereby granted, free of charge, to any person obtaining a copy
12
// of this software and associated documentation files (the "Software"), to deal
13
// in the Software without restriction, including without limitation the rights
14
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
// copies of the Software, and to permit persons to whom the Software is
16
// furnished to do so, subject to the following conditions:
17
//
18
// The above copyright notice and this permission notice shall be included in all
19
// copies or substantial portions of the Software.
20
//
21
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
// SOFTWARE.
28
//
29
// Thanks to Hugo Passos.
30
//
31
import 'dart:collection';
32
import 'dart:math';
33

34
import 'package:interval_tree/interval_tree.dart';
35

36
import 'item_selection_notifier.dart';
37

38
/// Manages a selection of items.
39
///
40
/// ItemSelection is an [Iterable] collection offering all standard iterable
41
/// operations, such as querying whether it [contains] specific indexes, easily
42
/// accessing the [first] or [last] index, or iterating all indexes in the
43
/// selection with [iterator].
44
///
45
/// It is often not necessary to create an ItemSelection instance yourself,
46
/// because [ItemSelectionController] will create one internally if necessary.
47
/// However, creating an ItemSelection instance gives you more control over the
48
/// selection. First of all, it allows you to specify an initial selection, and
49
/// secondly, you can listen to selection state changes for individual items.
50
///
51
/// ### Example
52
///
53
///     Widget build(BuildContext context) {
54
///       // specify initial selection
55
///       final mySelection = ItemSelection(0, 9);
56
///
57
///       // listen to selection changes
58
///       mySelection.addListener((int index, bool selected) {
59
///         print('$index: $selected');
60
///       });
61
///
62
///       // pass the selection to the controller
63
///       return ItemSelectionController(
64
///         selection: mySelection,
65
///         // ...
66
///       );
67
///     }
68
class ItemSelection extends ItemSelectionNotifier with IterableMixin<int?> {
69
  /// Creates a selection, optionally with an initial selection range from
70
  /// [start] to [end].
71 1
  ItemSelection([int? start, int? end]) {
72
    if (start != null) {
73 1
      _tree.add([start, end ?? start]);
74
    }
75
  }
76

77
  /// Creates a copy of the [other] selection.
78 1
  factory ItemSelection.copy(ItemSelection other) =>
79 1
      ItemSelection()..addAll(other);
80

81
  /// Returns `true` if this selection is empty.
82 1
  bool get isEmpty => _tree.isEmpty;
83

84
  /// Returns `true` if this selection is not empty.
85 1
  bool get isNotEmpty => _tree.isNotEmpty;
86

87
  /// Returns the first index in this selection.
88 1
  int get first => _tree.first.start;
89

90
  /// Returns the last index in this selection.
91 1
  int get last => _tree.last.end;
92

93
  /// Returns an iterator for iterating the indexes this selection.
94 1
  Iterator<int?> get iterator => _ItemSelectionIterator(_tree.iterator);
95

96
  /// Returns `true` if this selection contains the specified [index].
97 1
  bool contains(covariant int? index) {
98 1
    return _tree.contains([index, index]);
99
  }
100

101
  /// Adds a selection range from [start] to [end].
102 1
  void add(int start, [int? end]) {
103
    end ??= start;
104 1
    final addition = IntervalTree([start, end]);
105 1
    addition.removeAll(_tree.intersection(addition));
106 1
    for (final range in addition) {
107 1
      for (int i = range.start; i <= range.end; ++i) {
108 1
        notifyListeners(i, true);
109
      }
110
    }
111 1
    _tree.add([start, end]);
112
  }
113

114
  /// Adds all selection ranges to this selection,
115
  /// that are in the [other] selection.
116 1
  void addAll(ItemSelection other) {
117 1
    for (final iv in other._tree) {
118 1
      add(iv.start, iv.end);
119
    }
120
  }
121

122
  /// Removes the selection range from [start] to [end].
123 1
  void remove(int start, [int? end]) {
124 1
    if (_tree.isEmpty) return;
125 1
    start = max(start, first);
126 1
    end = min(end ?? start, last);
127 1
    final removal = _tree.intersection(IntervalTree([start, end]));
128 1
    for (final range in removal) {
129 1
      for (int i = range.start; i <= range.end; ++i) {
130 1
        notifyListeners(i, false);
131
      }
132
    }
133 1
    final startAtBounds = _tree.contains([start - 1, start - 1]) &&
134 1
        !_tree.contains([start - 2, start - 2]);
135 1
    final endAtBounds = _tree.contains([end + 1, end + 1]) &&
136 1
        !_tree.contains([end + 2, end + 2]);
137 1
    _tree.remove([start - 1, end + 1]);
138 1
    if (startAtBounds) _tree.add([start - 1, start - 1]);
139 1
    if (endAtBounds) _tree.add([end + 1, end + 1]);
140
  }
141

142
  /// Removes all selection ranges from this selection,
143
  /// that are in the [other] selection.
144 1
  void removeAll(ItemSelection other) {
145 1
    for (final iv in other._tree) {
146 1
      remove(iv.start, iv.end);
147
    }
148
  }
149

150
  /// Replaces the existing selection with a selection range from [start] to
151
  /// [end] so that no changes are notified for the overlapping range.
152 1
  void replace(int start, [int? end]) {
153
    end ??= start;
154

155 1
    final newTree = IntervalTree([start, end]);
156 1
    final overlap = _tree.intersection(newTree);
157

158 1
    final removal = IntervalTree.of(_tree);
159 1
    removal.removeAll(newTree);
160 1
    for (final range in removal) {
161 1
      for (int i = range.start; i <= range.end; ++i) {
162 1
        if (!overlap.contains([i, i])) {
163 1
          notifyListeners(i, false);
164
        }
165
      }
166
    }
167

168 1
    final addition = IntervalTree.of(newTree);
169 1
    addition.removeAll(removal);
170 1
    for (final range in addition) {
171 1
      for (int i = range.start; i <= range.end; ++i) {
172 1
        if (!overlap.contains([i, i])) {
173 1
          notifyListeners(i, true);
174
        }
175
      }
176
    }
177

178 1
    _tree = newTree;
179
  }
180

181
  /// Clears this selection.
182 1
  void clear() {
183 1
    for (final range in _tree) {
184 1
      for (int i = range.start; i <= range.end; ++i) {
185 1
        notifyListeners(i, false);
186
      }
187
    }
188 1
    _tree.clear();
189
  }
190

191
  IntervalTree _tree = IntervalTree();
192
}
193

194
class _ItemSelectionIterator extends Iterator<int?> {
195 1
  _ItemSelectionIterator(this._ranges);
196

197
  /// Returns the current value.
198 1
  @override
199 1
  int? get current => _current;
200

201
  /// Iterates to the next value and returns `true` on success, or `false`
202
  /// otherwise.
203 1
  @override
204
  bool moveNext() {
205 1
    if (_current == null) {
206 1
      if (!_ranges.moveNext()) return false;
207 1
      _current = _ranges.current.start;
208
    } else {
209 1
      _current = _current! + 1;
210
    }
211 1
    if (!_ranges.current.contains(Interval(_current, _current))) {
212 1
      if (!_ranges.moveNext()) {
213 1
        _current = null;
214
        return false;
215
      }
216 1
      _current = _ranges.current.start;
217
    }
218
    return true;
219
  }
220

221
  int? _current;
222
  Iterator<Interval> _ranges;
223
}

Read our documentation on viewing source code .

Loading