|
1 | | -from itertools import chain, product, groupby |
2 | | -from typing import List, Tuple, Iterable, Optional, Union |
| 1 | +from itertools import chain, starmap, product, groupby, islice |
| 2 | +from functools import reduce |
| 3 | +from operator import itemgetter |
| 4 | +from typing import List, Tuple, Iterable, Sequence, Optional, Union |
3 | 5 |
|
4 | 6 | from AnyQt.QtCore import ( |
5 | 7 | QModelIndex, QAbstractItemModel, QItemSelectionModel, QItemSelection, |
@@ -169,39 +171,95 @@ def ranges(indices): |
169 | 171 | yield start, end + 1 |
170 | 172 |
|
171 | 173 |
|
| 174 | +def merge_ranges( |
| 175 | + ranges: Iterable[Tuple[int, int]] |
| 176 | +) -> Sequence[Tuple[int, int]]: |
| 177 | + def merge_range_seq_accum( |
| 178 | + accum: List[Tuple[int, int]], r: Tuple[int, int] |
| 179 | + ) -> List[Tuple[int, int]]: |
| 180 | + last_start, last_stop = accum[-1] |
| 181 | + r_start, r_stop = r |
| 182 | + assert last_start <= r_start |
| 183 | + if r_start <= last_stop: |
| 184 | + # merge into last |
| 185 | + accum[-1] = last_start, max(last_stop, r_stop) |
| 186 | + else: |
| 187 | + # push a new (disconnected) range interval |
| 188 | + accum.append(r) |
| 189 | + return accum |
| 190 | + |
| 191 | + ranges = sorted(ranges, key=itemgetter(0)) |
| 192 | + if ranges: |
| 193 | + return reduce(merge_range_seq_accum, islice(ranges, 1, None), |
| 194 | + [ranges[0]]) |
| 195 | + else: |
| 196 | + return [] |
| 197 | + |
| 198 | + |
| 199 | +def qitemselection_select_range( |
| 200 | + selection: QItemSelection, |
| 201 | + model: QAbstractItemModel, |
| 202 | + rows: range, |
| 203 | + columns: range |
| 204 | +) -> None: |
| 205 | + assert rows.step == 1 and columns.step == 1 |
| 206 | + selection.select( |
| 207 | + model.index(rows.start, columns.start), |
| 208 | + model.index(rows.stop - 1, columns.stop - 1) |
| 209 | + ) |
| 210 | + |
| 211 | + |
172 | 212 | class SymmetricSelectionModel(QItemSelectionModel): |
173 | | - def select(self, selection, flags): |
| 213 | + """ |
| 214 | + Item selection model ensuring the selection is symmetric |
| 215 | +
|
| 216 | + """ |
| 217 | + def select(self, selection: Union[QItemSelection, QModelIndex], |
| 218 | + flags: QItemSelectionModel.SelectionFlags) -> None: |
| 219 | + def to_ranges(rngs: Iterable[Tuple[int, int]]) -> Sequence[range]: |
| 220 | + return list(starmap(range, rngs)) |
174 | 221 | if isinstance(selection, QModelIndex): |
175 | 222 | selection = QItemSelection(selection, selection) |
176 | 223 |
|
| 224 | + if flags & QItemSelectionModel.Current: # no current selection support |
| 225 | + flags &= ~QItemSelectionModel.Current |
| 226 | + if flags & QItemSelectionModel.Toggle: # no toggle support either |
| 227 | + flags &= ~QItemSelectionModel.Toggle |
| 228 | + flags |= QItemSelectionModel.Select |
| 229 | + |
177 | 230 | model = self.model() |
178 | | - indexes = selection.indexes() |
179 | | - sel_inds = {ind.row() for ind in indexes} | \ |
180 | | - {ind.column() for ind in indexes} |
| 231 | + rows, cols = selection_blocks(selection) |
| 232 | + sym_ranges = to_ranges(merge_ranges(chain(rows, cols))) |
181 | 233 | if flags == QItemSelectionModel.ClearAndSelect: |
182 | | - selected = set() |
183 | | - else: |
184 | | - selected = {ind.row() for ind in self.selectedIndexes()} |
185 | | - if flags & QItemSelectionModel.Select: |
186 | | - selected |= sel_inds |
187 | | - elif flags & QItemSelectionModel.Deselect: |
188 | | - selected -= sel_inds |
189 | | - new_selection = QItemSelection() |
190 | | - regions = list(ranges(sorted(selected))) |
191 | | - for r_start, r_end in regions: |
192 | | - for c_start, c_end in regions: |
193 | | - top_left = model.index(r_start, c_start) |
194 | | - bottom_right = model.index(r_end - 1, c_end - 1) |
195 | | - new_selection.select(top_left, bottom_right) |
196 | | - QItemSelectionModel.select(self, new_selection, |
197 | | - QItemSelectionModel.ClearAndSelect) |
198 | | - |
199 | | - def selected_items(self): |
200 | | - return list({ind.row() for ind in self.selectedIndexes()}) |
201 | | - |
202 | | - def set_selected_items(self, inds): |
203 | | - index = self.model().index |
| 234 | + # extend ranges in `selection` to symmetric selection |
| 235 | + # row/columns. |
| 236 | + selection = QItemSelection() |
| 237 | + for rows, cols in product(sym_ranges, sym_ranges): |
| 238 | + qitemselection_select_range(selection, model, rows, cols) |
| 239 | + elif flags & (QItemSelectionModel.Select | |
| 240 | + QItemSelectionModel.Deselect): |
| 241 | + # extend ranges in sym_ranges to span all current rows/columns |
| 242 | + rows_current, cols_current = selection_blocks(self.selection()) |
| 243 | + ext_selection = QItemSelection() |
| 244 | + for rrange, crange in product(sym_ranges, sym_ranges): |
| 245 | + qitemselection_select_range(selection, model, rrange, crange) |
| 246 | + for rrange, crange in product(sym_ranges, to_ranges(cols_current)): |
| 247 | + qitemselection_select_range(selection, model, rrange, crange) |
| 248 | + for rrange, crange in product(to_ranges(rows_current), sym_ranges): |
| 249 | + qitemselection_select_range(selection, model, rrange, crange) |
| 250 | + selection.merge(ext_selection, QItemSelectionModel.Select) |
| 251 | + super().select(selection, flags) |
| 252 | + |
| 253 | + def selectedItems(self) -> Sequence[int]: |
| 254 | + """Return the indices of the the symmetric selection.""" |
| 255 | + ranges_ = starmap(range, selection_rows(self.selection())) |
| 256 | + return sorted(chain.from_iterable(ranges_)) |
| 257 | + |
| 258 | + def setSelectedItems(self, inds: Iterable[int]): |
| 259 | + """Set and select the `inds` indices""" |
| 260 | + model = self.model() |
204 | 261 | selection = QItemSelection() |
205 | | - for i in inds: |
206 | | - selection.select(index(i, i), index(i, i)) |
| 262 | + sym_ranges = to_ranges(ranges(inds)) |
| 263 | + for rows, cols in product(sym_ranges, sym_ranges): |
| 264 | + qitemselection_select_range(selection, model, rows, cols) |
207 | 265 | self.select(selection, QItemSelectionModel.ClearAndSelect) |
0 commit comments