Skip to content

Commit 33dac33

Browse files
committed
SymmetricSelectionModel: Optimize selection
Operate on selection ranges not on indices
1 parent aac746b commit 33dac33

File tree

3 files changed

+116
-37
lines changed

3 files changed

+116
-37
lines changed

Orange/widgets/unsupervised/owdistancematrix.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,12 @@ def settings_from_widget(self, widget, *args):
168168
context = widget.current_context
169169
if context is not None:
170170
context.annotation = widget.annot_combo.currentText()
171-
context.selection = widget.tableview.selectionModel().selected_items()
171+
context.selection = widget.tableview.selectionModel().selectedItems()
172172

173173
def settings_to_widget(self, widget, *args):
174174
context = widget.current_context
175175
widget.annotation_idx = context.annotations.index(context.annotation)
176-
widget.tableview.selectionModel().set_selected_items(context.selection)
176+
widget.tableview.selectionModel().setSelectedItems(context.selection)
177177

178178

179179
class OWDistanceMatrix(widget.OWWidget):
@@ -245,7 +245,7 @@ def set_distances(self, distances):
245245
self.distances = distances
246246
self.tablemodel.set_data(self.distances)
247247
self.selection = []
248-
self.tableview.selectionModel().set_selected_items([])
248+
self.tableview.selectionModel().clear()
249249

250250
self.items = items = distances is not None and distances.row_items
251251
annotations = ["None", "Enumerate"]
@@ -291,7 +291,7 @@ def _update_labels(self):
291291
var = self.annot_combo.model()[self.annotation_idx]
292292
column, _ = self.items.get_column_view(var)
293293
labels = [var.str_val(value) for value in column]
294-
saved_selection = self.tableview.selectionModel().selected_items()
294+
saved_selection = self.tableview.selectionModel().selectedIndices()
295295
self.tablemodel.set_labels(labels, var, column)
296296
if labels:
297297
self.tableview.horizontalHeader().show()
@@ -305,7 +305,7 @@ def _update_labels(self):
305305
def commit(self):
306306
sub_table = sub_distances = None
307307
if self.distances is not None:
308-
inds = self.tableview.selectionModel().selected_items()
308+
inds = self.tableview.selectionModel().selectedItems()
309309
if inds:
310310
sub_distances = self.distances.submatrix(inds)
311311
if self.distances.axis and isinstance(self.items, Table):

Orange/widgets/utils/itemselectionmodel.py

Lines changed: 88 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
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
35

46
from AnyQt.QtCore import (
57
QModelIndex, QAbstractItemModel, QItemSelectionModel, QItemSelection,
@@ -169,39 +171,95 @@ def ranges(indices):
169171
yield start, end + 1
170172

171173

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+
172212
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))
174221
if isinstance(selection, QModelIndex):
175222
selection = QItemSelection(selection, selection)
176223

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+
177230
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)))
181233
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()
204261
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)
207265
self.select(selection, QItemSelectionModel.ClearAndSelect)

Orange/widgets/utils/tests/test_itemselectionmodel.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
from AnyQt.QtCore import QItemSelectionModel
55
from AnyQt.QtGui import QStandardItemModel
66

7-
from Orange.widgets.utils.itemselectionmodel import BlockSelectionModel
7+
from Orange.widgets.utils.itemselectionmodel import BlockSelectionModel, \
8+
SymmetricSelectionModel
89

910

1011
def selected(sel: QItemSelectionModel) -> Set[Tuple[int, int]]:
1112
return set((r.row(), r.column()) for r in sel.selectedIndexes())
1213

1314

14-
class BlockSelectionModelTest(TestCase):
15+
class TestBlockSelectionModel(TestCase):
1516
def test_blockselectionmodel(self):
1617
model = QStandardItemModel()
1718
model.setRowCount(4)
@@ -27,3 +28,23 @@ def test_blockselectionmodel(self):
2728
self.assertSetEqual(selected(sel), {(1, 1)})
2829
sel.select(model.index(3, 3), BlockSelectionModel.ClearAndSelect)
2930
self.assertSetEqual(selected(sel), {(3, 3)})
31+
32+
33+
class TestSymmetricSelectionModel(TestCase):
34+
def test_symmetricselectionmodel(self):
35+
model = QStandardItemModel()
36+
model.setRowCount(4)
37+
model.setColumnCount(4)
38+
sel = SymmetricSelectionModel(model)
39+
sel.select(model.index(0, 0), BlockSelectionModel.Select)
40+
self.assertSetEqual(selected(sel), {(0, 0)})
41+
sel.select(model.index(0, 2), BlockSelectionModel.Select)
42+
self.assertSetEqual(selected(sel), {(0, 0), (0, 2), (2, 0), (2, 2)})
43+
sel.select(model.index(0, 0), BlockSelectionModel.Deselect)
44+
self.assertSetEqual(selected(sel), {(2, 2)})
45+
sel.select(model.index(2, 3), BlockSelectionModel.ClearAndSelect)
46+
self.assertSetEqual(selected(sel), {(2, 2), (2, 3), (3, 2), (3, 3)})
47+
48+
self.assertSetEqual(set(sel.selectedItems()), {2, 3})
49+
sel.setSelectedItems([1, 2])
50+
self.assertSetEqual(set(sel.selectedItems()), {1, 2})

0 commit comments

Comments
 (0)