From 84bb0dc531ddf6b85bce92709726342be6737cb8 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Mon, 11 Jan 2021 13:17:24 +0100 Subject: [PATCH 1/5] itemselectionmodel: Move BlockSelectionModel to utils --- Orange/widgets/data/owtable.py | 163 +---------------- Orange/widgets/utils/itemselectionmodel.py | 169 ++++++++++++++++++ .../utils/tests/test_itemselectionmodel.py | 29 +++ 3 files changed, 201 insertions(+), 160 deletions(-) create mode 100644 Orange/widgets/utils/itemselectionmodel.py create mode 100644 Orange/widgets/utils/tests/test_itemselectionmodel.py diff --git a/Orange/widgets/data/owtable.py b/Orange/widgets/data/owtable.py index 965592d8488..8fd753609e1 100644 --- a/Orange/widgets/data/owtable.py +++ b/Orange/widgets/data/owtable.py @@ -6,7 +6,6 @@ import concurrent.futures from collections import OrderedDict, namedtuple -from typing import List, Tuple, Iterable from math import isnan @@ -33,6 +32,9 @@ from Orange.widgets import gui from Orange.widgets.settings import Setting +from Orange.widgets.utils.itemselectionmodel import ( + BlockSelectionModel, ranges, selection_blocks +) from Orange.widgets.utils.tableview import TableView from Orange.widgets.utils.widgetpreview import WidgetPreview from Orange.widgets.widget import OWWidget, Input, Output @@ -161,165 +163,6 @@ def rowCount(self, parent=QModelIndex()): return stop - start -class BlockSelectionModel(QItemSelectionModel): - """ - Item selection model ensuring the selection maintains a simple block - like structure. - - e.g. - - [a b] c [d e] - [f g] h [i j] - - is allowed but this is not - - [a] b c d e - [f g] h [i j] - - I.e. select the Cartesian product of row and column indices. - - """ - def __init__(self, model, parent=None, selectBlocks=True, **kwargs): - super().__init__(model, parent, **kwargs) - self.__selectBlocks = selectBlocks - - def select(self, selection, flags): - """Reimplemented.""" - if isinstance(selection, QModelIndex): - selection = QItemSelection(selection, selection) - - if not self.__selectBlocks: - super().select(selection, flags) - return - - model = self.model() - - def to_ranges(spans): - return list(range(*r) for r in spans) - - if flags & QItemSelectionModel.Current: # no current selection support - flags &= ~QItemSelectionModel.Current - if flags & QItemSelectionModel.Toggle: # no toggle support either - flags &= ~QItemSelectionModel.Toggle - flags |= QItemSelectionModel.Select - - if flags == QItemSelectionModel.ClearAndSelect: - # extend selection ranges in `selection` to span all row/columns - sel_rows = selection_rows(selection) - sel_cols = selection_columns(selection) - selection = QItemSelection() - for row_range, col_range in \ - itertools.product(to_ranges(sel_rows), to_ranges(sel_cols)): - selection.select( - model.index(row_range.start, col_range.start), - model.index(row_range.stop - 1, col_range.stop - 1) - ) - elif flags & (QItemSelectionModel.Select | - QItemSelectionModel.Deselect): - # extend all selection ranges in `selection` with the full current - # row/col spans - rows, cols = selection_blocks(self.selection()) - sel_rows = selection_rows(selection) - sel_cols = selection_columns(selection) - ext_selection = QItemSelection() - for row_range, col_range in \ - itertools.product(to_ranges(rows), to_ranges(sel_cols)): - ext_selection.select( - model.index(row_range.start, col_range.start), - model.index(row_range.stop - 1, col_range.stop - 1) - ) - for row_range, col_range in \ - itertools.product(to_ranges(sel_rows), to_ranges(cols)): - ext_selection.select( - model.index(row_range.start, col_range.start), - model.index(row_range.stop - 1, col_range.stop - 1) - ) - selection.merge(ext_selection, QItemSelectionModel.Select) - super().select(selection, flags) - - def selectBlocks(self): - """Is the block selection in effect.""" - return self.__selectBlocks - - def setSelectBlocks(self, state): - """Set the block selection state. - - If set to False, the selection model behaves as the base - QItemSelectionModel - - """ - self.__selectBlocks = state - - -def selection_rows(selection): - # type: (QItemSelection) -> List[Tuple[int, int]] - """ - Return a list of ranges for all referenced rows contained in selection - - Parameters - ---------- - selection : QItemSelection - - Returns - ------- - rows : List[Tuple[int, int]] - """ - spans = set(range(s.top(), s.bottom() + 1) for s in selection) - indices = sorted(set(itertools.chain(*spans))) - return list(ranges(indices)) - - -def selection_columns(selection): - # type: (QItemSelection) -> List[Tuple[int, int]] - """ - Return a list of ranges for all referenced columns contained in selection - - Parameters - ---------- - selection : QItemSelection - - Returns - ------- - rows : List[Tuple[int, int]] - """ - spans = {range(s.left(), s.right() + 1) for s in selection} - indices = sorted(set(itertools.chain(*spans))) - return list(ranges(indices)) - - -def selection_blocks(selection): - # type: (QItemSelection) -> Tuple[List[Tuple[int, int]], List[Tuple[int, int]]] - if selection.count() > 0: - rowranges = {range(span.top(), span.bottom() + 1) - for span in selection} - colranges = {range(span.left(), span.right() + 1) - for span in selection} - else: - return [], [] - - rows = sorted(set(itertools.chain(*rowranges))) - cols = sorted(set(itertools.chain(*colranges))) - return list(ranges(rows)), list(ranges(cols)) - - -def ranges(indices): - # type: (Iterable[int]) -> Iterable[Tuple[int, int]] - """ - Group consecutive indices into `(start, stop)` tuple 'ranges'. - - >>> list(ranges([1, 2, 3, 5, 3, 4])) - >>> [(1, 4), (5, 6), (3, 5)] - - """ - g = itertools.groupby(enumerate(indices), - key=lambda t: t[1] - t[0]) - for _, range_ind in g: - range_ind = list(range_ind) - _, start = range_ind[0] - _, end = range_ind[-1] - yield start, end + 1 - - def table_selection_to_mime_data(table): """Copy the current selection in a QTableView to the clipboard. """ diff --git a/Orange/widgets/utils/itemselectionmodel.py b/Orange/widgets/utils/itemselectionmodel.py new file mode 100644 index 00000000000..713f298592a --- /dev/null +++ b/Orange/widgets/utils/itemselectionmodel.py @@ -0,0 +1,169 @@ +from itertools import chain, product, groupby +from typing import List, Tuple, Iterable, Optional, Union + +from AnyQt.QtCore import ( + QModelIndex, QAbstractItemModel, QItemSelectionModel, QItemSelection, + QObject +) + + +class BlockSelectionModel(QItemSelectionModel): + """ + Item selection model ensuring the selection maintains a simple block + like structure. + + e.g. + + [a b] c [d e] + [f g] h [i j] + + is allowed but this is not + + [a] b c d e + [f g] h [i j] + + I.e. select the Cartesian product of row and column indices. + + """ + def __init__( + self, model: QAbstractItemModel, parent: Optional[QObject] = None, + selectBlocks=True, **kwargs + ) -> None: + super().__init__(model, parent, **kwargs) + self.__selectBlocks = selectBlocks + + def select(self, selection: Union[QItemSelection, QModelIndex], + flags: QItemSelectionModel.SelectionFlags) -> None: + """Reimplemented.""" + if isinstance(selection, QModelIndex): + selection = QItemSelection(selection, selection) + + if not self.__selectBlocks: + super().select(selection, flags) + return + + model = self.model() + + def to_ranges(spans): + return list(range(*r) for r in spans) + + if flags & QItemSelectionModel.Current: # no current selection support + flags &= ~QItemSelectionModel.Current + if flags & QItemSelectionModel.Toggle: # no toggle support either + flags &= ~QItemSelectionModel.Toggle + flags |= QItemSelectionModel.Select + + if flags == QItemSelectionModel.ClearAndSelect: + # extend selection ranges in `selection` to span all row/columns + sel_rows = selection_rows(selection) + sel_cols = selection_columns(selection) + selection = QItemSelection() + for row_range, col_range in \ + product(to_ranges(sel_rows), to_ranges(sel_cols)): + selection.select( + model.index(row_range.start, col_range.start), + model.index(row_range.stop - 1, col_range.stop - 1) + ) + elif flags & (QItemSelectionModel.Select | + QItemSelectionModel.Deselect): + # extend all selection ranges in `selection` with the full current + # row/col spans + rows, cols = selection_blocks(self.selection()) + sel_rows = selection_rows(selection) + sel_cols = selection_columns(selection) + ext_selection = QItemSelection() + for row_range, col_range in \ + product(to_ranges(rows), to_ranges(sel_cols)): + ext_selection.select( + model.index(row_range.start, col_range.start), + model.index(row_range.stop - 1, col_range.stop - 1) + ) + for row_range, col_range in \ + product(to_ranges(sel_rows), to_ranges(cols)): + ext_selection.select( + model.index(row_range.start, col_range.start), + model.index(row_range.stop - 1, col_range.stop - 1) + ) + selection.merge(ext_selection, QItemSelectionModel.Select) + super().select(selection, flags) + + def selectBlocks(self): + """Is the block selection in effect.""" + return self.__selectBlocks + + def setSelectBlocks(self, state): + """Set the block selection state. + + If set to False, the selection model behaves as the base + QItemSelectionModel + + """ + self.__selectBlocks = state + + +def selection_rows(selection): + # type: (QItemSelection) -> List[Tuple[int, int]] + """ + Return a list of ranges for all referenced rows contained in selection + + Parameters + ---------- + selection : QItemSelection + + Returns + ------- + rows : List[Tuple[int, int]] + """ + spans = set(range(s.top(), s.bottom() + 1) for s in selection) + indices = sorted(set(chain.from_iterable(spans))) + return list(ranges(indices)) + + +def selection_columns(selection): + # type: (QItemSelection) -> List[Tuple[int, int]] + """ + Return a list of ranges for all referenced columns contained in selection + + Parameters + ---------- + selection : QItemSelection + + Returns + ------- + rows : List[Tuple[int, int]] + """ + spans = {range(s.left(), s.right() + 1) for s in selection} + indices = sorted(set(chain.from_iterable(spans))) + return list(ranges(indices)) + + +def selection_blocks(selection): + # type: (QItemSelection) -> Tuple[List[Tuple[int, int]], List[Tuple[int, int]]] + if selection.count() > 0: + rowranges = {range(span.top(), span.bottom() + 1) + for span in selection} + colranges = {range(span.left(), span.right() + 1) + for span in selection} + else: + return [], [] + + rows = sorted(set(chain.from_iterable(rowranges))) + cols = sorted(set(chain.from_iterable(colranges))) + return list(ranges(rows)), list(ranges(cols)) + + +def ranges(indices): + # type: (Iterable[int]) -> Iterable[Tuple[int, int]] + """ + Group consecutive indices into `(start, stop)` tuple 'ranges'. + + >>> list(ranges([1, 2, 3, 5, 3, 4])) + >>> [(1, 4), (5, 6), (3, 5)] + + """ + g = groupby(enumerate(indices), key=lambda t: t[1] - t[0]) + for _, range_ind in g: + range_ind = list(range_ind) + _, start = range_ind[0] + _, end = range_ind[-1] + yield start, end + 1 diff --git a/Orange/widgets/utils/tests/test_itemselectionmodel.py b/Orange/widgets/utils/tests/test_itemselectionmodel.py new file mode 100644 index 00000000000..d5767e6bd4a --- /dev/null +++ b/Orange/widgets/utils/tests/test_itemselectionmodel.py @@ -0,0 +1,29 @@ +from unittest import TestCase +from typing import Tuple, Set + +from AnyQt.QtCore import QItemSelectionModel +from AnyQt.QtGui import QStandardItemModel + +from Orange.widgets.utils.itemselectionmodel import BlockSelectionModel + + +def selected(sel: QItemSelectionModel) -> Set[Tuple[int, int]]: + return set((r.row(), r.column()) for r in sel.selectedIndexes()) + + +class BlockSelectionModelTest(TestCase): + def test_blockselectionmodel(self): + model = QStandardItemModel() + model.setRowCount(4) + model.setColumnCount(4) + sel = BlockSelectionModel(model) + sel.select(model.index(0, 0), BlockSelectionModel.Select) + self.assertSetEqual(selected(sel), {(0, 0)}) + sel.select(model.index(0, 1), BlockSelectionModel.Select) + self.assertSetEqual(selected(sel), {(0, 0), (0, 1)}) + sel.select(model.index(1, 1), BlockSelectionModel.Select) + self.assertSetEqual(selected(sel), {(0, 0), (0, 1), (1, 0), (1, 1)}) + sel.select(model.index(0, 0), BlockSelectionModel.Deselect) + self.assertSetEqual(selected(sel), {(1, 1)}) + sel.select(model.index(3, 3), BlockSelectionModel.ClearAndSelect) + self.assertSetEqual(selected(sel), {(3, 3)}) From aac746bea65c1a524917652edf517598f20d2c9c Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Tue, 12 Jan 2021 08:57:33 +0100 Subject: [PATCH 2/5] itemselectionmodel: Move SymmetricSelectionModel to util --- .../widgets/unsupervised/owdistancematrix.py | 43 +------------------ Orange/widgets/utils/itemselectionmodel.py | 38 ++++++++++++++++ 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/Orange/widgets/unsupervised/owdistancematrix.py b/Orange/widgets/unsupervised/owdistancematrix.py index ad7b93909e2..b10f07d986f 100644 --- a/Orange/widgets/unsupervised/owdistancematrix.py +++ b/Orange/widgets/unsupervised/owdistancematrix.py @@ -6,16 +6,15 @@ from AnyQt.QtWidgets import QTableView, QItemDelegate, QHeaderView, QStyle, \ QStyleOptionViewItem from AnyQt.QtGui import QColor, QPen, QBrush -from AnyQt.QtCore import Qt, QAbstractTableModel, QModelIndex, \ - QItemSelectionModel, QItemSelection, QSize +from AnyQt.QtCore import Qt, QAbstractTableModel, QSize from Orange.data import Table, Variable, StringVariable from Orange.misc import DistMatrix from Orange.widgets import widget, gui -from Orange.widgets.data.owtable import ranges from Orange.widgets.gui import OrangeUserRole from Orange.widgets.settings import Setting, ContextSetting, ContextHandler from Orange.widgets.utils.itemmodels import VariableListModel +from Orange.widgets.utils.itemselectionmodel import SymmetricSelectionModel from Orange.widgets.utils.widgetpreview import WidgetPreview from Orange.widgets.widget import Input, Output @@ -121,44 +120,6 @@ def paint(self, painter, option, index): painter.restore() -class SymmetricSelectionModel(QItemSelectionModel): - def select(self, selection, flags): - if isinstance(selection, QModelIndex): - selection = QItemSelection(selection, selection) - - model = self.model() - indexes = selection.indexes() - sel_inds = {ind.row() for ind in indexes} | \ - {ind.column() for ind in indexes} - if flags == QItemSelectionModel.ClearAndSelect: - selected = set() - else: - selected = {ind.row() for ind in self.selectedIndexes()} - if flags & QItemSelectionModel.Select: - selected |= sel_inds - elif flags & QItemSelectionModel.Deselect: - selected -= sel_inds - new_selection = QItemSelection() - regions = list(ranges(sorted(selected))) - for r_start, r_end in regions: - for c_start, c_end in regions: - top_left = model.index(r_start, c_start) - bottom_right = model.index(r_end - 1, c_end - 1) - new_selection.select(top_left, bottom_right) - QItemSelectionModel.select(self, new_selection, - QItemSelectionModel.ClearAndSelect) - - def selected_items(self): - return list({ind.row() for ind in self.selectedIndexes()}) - - def set_selected_items(self, inds): - index = self.model().index - selection = QItemSelection() - for i in inds: - selection.select(index(i, i), index(i, i)) - self.select(selection, QItemSelectionModel.ClearAndSelect) - - class TableView(gui.HScrollStepMixin, QTableView): def sizeHintForColumn(self, column: int) -> int: model = self.model() diff --git a/Orange/widgets/utils/itemselectionmodel.py b/Orange/widgets/utils/itemselectionmodel.py index 713f298592a..46db769671d 100644 --- a/Orange/widgets/utils/itemselectionmodel.py +++ b/Orange/widgets/utils/itemselectionmodel.py @@ -167,3 +167,41 @@ def ranges(indices): _, start = range_ind[0] _, end = range_ind[-1] yield start, end + 1 + + +class SymmetricSelectionModel(QItemSelectionModel): + def select(self, selection, flags): + if isinstance(selection, QModelIndex): + selection = QItemSelection(selection, selection) + + model = self.model() + indexes = selection.indexes() + sel_inds = {ind.row() for ind in indexes} | \ + {ind.column() for ind in indexes} + if flags == QItemSelectionModel.ClearAndSelect: + selected = set() + else: + selected = {ind.row() for ind in self.selectedIndexes()} + if flags & QItemSelectionModel.Select: + selected |= sel_inds + elif flags & QItemSelectionModel.Deselect: + selected -= sel_inds + new_selection = QItemSelection() + regions = list(ranges(sorted(selected))) + for r_start, r_end in regions: + for c_start, c_end in regions: + top_left = model.index(r_start, c_start) + bottom_right = model.index(r_end - 1, c_end - 1) + new_selection.select(top_left, bottom_right) + QItemSelectionModel.select(self, new_selection, + QItemSelectionModel.ClearAndSelect) + + def selected_items(self): + return list({ind.row() for ind in self.selectedIndexes()}) + + def set_selected_items(self, inds): + index = self.model().index + selection = QItemSelection() + for i in inds: + selection.select(index(i, i), index(i, i)) + self.select(selection, QItemSelectionModel.ClearAndSelect) From 33dac33fa721a144075d8d0ef898e152d98e48c1 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Tue, 12 Jan 2021 10:27:29 +0100 Subject: [PATCH 3/5] SymmetricSelectionModel: Optimize selection Operate on selection ranges not on indices --- .../widgets/unsupervised/owdistancematrix.py | 10 +- Orange/widgets/utils/itemselectionmodel.py | 118 +++++++++++++----- .../utils/tests/test_itemselectionmodel.py | 25 +++- 3 files changed, 116 insertions(+), 37 deletions(-) diff --git a/Orange/widgets/unsupervised/owdistancematrix.py b/Orange/widgets/unsupervised/owdistancematrix.py index b10f07d986f..ea84df90b70 100644 --- a/Orange/widgets/unsupervised/owdistancematrix.py +++ b/Orange/widgets/unsupervised/owdistancematrix.py @@ -168,12 +168,12 @@ def settings_from_widget(self, widget, *args): context = widget.current_context if context is not None: context.annotation = widget.annot_combo.currentText() - context.selection = widget.tableview.selectionModel().selected_items() + context.selection = widget.tableview.selectionModel().selectedItems() def settings_to_widget(self, widget, *args): context = widget.current_context widget.annotation_idx = context.annotations.index(context.annotation) - widget.tableview.selectionModel().set_selected_items(context.selection) + widget.tableview.selectionModel().setSelectedItems(context.selection) class OWDistanceMatrix(widget.OWWidget): @@ -245,7 +245,7 @@ def set_distances(self, distances): self.distances = distances self.tablemodel.set_data(self.distances) self.selection = [] - self.tableview.selectionModel().set_selected_items([]) + self.tableview.selectionModel().clear() self.items = items = distances is not None and distances.row_items annotations = ["None", "Enumerate"] @@ -291,7 +291,7 @@ def _update_labels(self): var = self.annot_combo.model()[self.annotation_idx] column, _ = self.items.get_column_view(var) labels = [var.str_val(value) for value in column] - saved_selection = self.tableview.selectionModel().selected_items() + saved_selection = self.tableview.selectionModel().selectedIndices() self.tablemodel.set_labels(labels, var, column) if labels: self.tableview.horizontalHeader().show() @@ -305,7 +305,7 @@ def _update_labels(self): def commit(self): sub_table = sub_distances = None if self.distances is not None: - inds = self.tableview.selectionModel().selected_items() + inds = self.tableview.selectionModel().selectedItems() if inds: sub_distances = self.distances.submatrix(inds) if self.distances.axis and isinstance(self.items, Table): diff --git a/Orange/widgets/utils/itemselectionmodel.py b/Orange/widgets/utils/itemselectionmodel.py index 46db769671d..e23a242635b 100644 --- a/Orange/widgets/utils/itemselectionmodel.py +++ b/Orange/widgets/utils/itemselectionmodel.py @@ -1,5 +1,7 @@ -from itertools import chain, product, groupby -from typing import List, Tuple, Iterable, Optional, Union +from itertools import chain, starmap, product, groupby, islice +from functools import reduce +from operator import itemgetter +from typing import List, Tuple, Iterable, Sequence, Optional, Union from AnyQt.QtCore import ( QModelIndex, QAbstractItemModel, QItemSelectionModel, QItemSelection, @@ -169,39 +171,95 @@ def ranges(indices): yield start, end + 1 +def merge_ranges( + ranges: Iterable[Tuple[int, int]] +) -> Sequence[Tuple[int, int]]: + def merge_range_seq_accum( + accum: List[Tuple[int, int]], r: Tuple[int, int] + ) -> List[Tuple[int, int]]: + last_start, last_stop = accum[-1] + r_start, r_stop = r + assert last_start <= r_start + if r_start <= last_stop: + # merge into last + accum[-1] = last_start, max(last_stop, r_stop) + else: + # push a new (disconnected) range interval + accum.append(r) + return accum + + ranges = sorted(ranges, key=itemgetter(0)) + if ranges: + return reduce(merge_range_seq_accum, islice(ranges, 1, None), + [ranges[0]]) + else: + return [] + + +def qitemselection_select_range( + selection: QItemSelection, + model: QAbstractItemModel, + rows: range, + columns: range +) -> None: + assert rows.step == 1 and columns.step == 1 + selection.select( + model.index(rows.start, columns.start), + model.index(rows.stop - 1, columns.stop - 1) + ) + + class SymmetricSelectionModel(QItemSelectionModel): - def select(self, selection, flags): + """ + Item selection model ensuring the selection is symmetric + + """ + def select(self, selection: Union[QItemSelection, QModelIndex], + flags: QItemSelectionModel.SelectionFlags) -> None: + def to_ranges(rngs: Iterable[Tuple[int, int]]) -> Sequence[range]: + return list(starmap(range, rngs)) if isinstance(selection, QModelIndex): selection = QItemSelection(selection, selection) + if flags & QItemSelectionModel.Current: # no current selection support + flags &= ~QItemSelectionModel.Current + if flags & QItemSelectionModel.Toggle: # no toggle support either + flags &= ~QItemSelectionModel.Toggle + flags |= QItemSelectionModel.Select + model = self.model() - indexes = selection.indexes() - sel_inds = {ind.row() for ind in indexes} | \ - {ind.column() for ind in indexes} + rows, cols = selection_blocks(selection) + sym_ranges = to_ranges(merge_ranges(chain(rows, cols))) if flags == QItemSelectionModel.ClearAndSelect: - selected = set() - else: - selected = {ind.row() for ind in self.selectedIndexes()} - if flags & QItemSelectionModel.Select: - selected |= sel_inds - elif flags & QItemSelectionModel.Deselect: - selected -= sel_inds - new_selection = QItemSelection() - regions = list(ranges(sorted(selected))) - for r_start, r_end in regions: - for c_start, c_end in regions: - top_left = model.index(r_start, c_start) - bottom_right = model.index(r_end - 1, c_end - 1) - new_selection.select(top_left, bottom_right) - QItemSelectionModel.select(self, new_selection, - QItemSelectionModel.ClearAndSelect) - - def selected_items(self): - return list({ind.row() for ind in self.selectedIndexes()}) - - def set_selected_items(self, inds): - index = self.model().index + # extend ranges in `selection` to symmetric selection + # row/columns. + selection = QItemSelection() + for rows, cols in product(sym_ranges, sym_ranges): + qitemselection_select_range(selection, model, rows, cols) + elif flags & (QItemSelectionModel.Select | + QItemSelectionModel.Deselect): + # extend ranges in sym_ranges to span all current rows/columns + rows_current, cols_current = selection_blocks(self.selection()) + ext_selection = QItemSelection() + for rrange, crange in product(sym_ranges, sym_ranges): + qitemselection_select_range(selection, model, rrange, crange) + for rrange, crange in product(sym_ranges, to_ranges(cols_current)): + qitemselection_select_range(selection, model, rrange, crange) + for rrange, crange in product(to_ranges(rows_current), sym_ranges): + qitemselection_select_range(selection, model, rrange, crange) + selection.merge(ext_selection, QItemSelectionModel.Select) + super().select(selection, flags) + + def selectedItems(self) -> Sequence[int]: + """Return the indices of the the symmetric selection.""" + ranges_ = starmap(range, selection_rows(self.selection())) + return sorted(chain.from_iterable(ranges_)) + + def setSelectedItems(self, inds: Iterable[int]): + """Set and select the `inds` indices""" + model = self.model() selection = QItemSelection() - for i in inds: - selection.select(index(i, i), index(i, i)) + sym_ranges = to_ranges(ranges(inds)) + for rows, cols in product(sym_ranges, sym_ranges): + qitemselection_select_range(selection, model, rows, cols) self.select(selection, QItemSelectionModel.ClearAndSelect) diff --git a/Orange/widgets/utils/tests/test_itemselectionmodel.py b/Orange/widgets/utils/tests/test_itemselectionmodel.py index d5767e6bd4a..1265ac01aaf 100644 --- a/Orange/widgets/utils/tests/test_itemselectionmodel.py +++ b/Orange/widgets/utils/tests/test_itemselectionmodel.py @@ -4,14 +4,15 @@ from AnyQt.QtCore import QItemSelectionModel from AnyQt.QtGui import QStandardItemModel -from Orange.widgets.utils.itemselectionmodel import BlockSelectionModel +from Orange.widgets.utils.itemselectionmodel import BlockSelectionModel, \ + SymmetricSelectionModel def selected(sel: QItemSelectionModel) -> Set[Tuple[int, int]]: return set((r.row(), r.column()) for r in sel.selectedIndexes()) -class BlockSelectionModelTest(TestCase): +class TestBlockSelectionModel(TestCase): def test_blockselectionmodel(self): model = QStandardItemModel() model.setRowCount(4) @@ -27,3 +28,23 @@ def test_blockselectionmodel(self): self.assertSetEqual(selected(sel), {(1, 1)}) sel.select(model.index(3, 3), BlockSelectionModel.ClearAndSelect) self.assertSetEqual(selected(sel), {(3, 3)}) + + +class TestSymmetricSelectionModel(TestCase): + def test_symmetricselectionmodel(self): + model = QStandardItemModel() + model.setRowCount(4) + model.setColumnCount(4) + sel = SymmetricSelectionModel(model) + sel.select(model.index(0, 0), BlockSelectionModel.Select) + self.assertSetEqual(selected(sel), {(0, 0)}) + sel.select(model.index(0, 2), BlockSelectionModel.Select) + self.assertSetEqual(selected(sel), {(0, 0), (0, 2), (2, 0), (2, 2)}) + sel.select(model.index(0, 0), BlockSelectionModel.Deselect) + self.assertSetEqual(selected(sel), {(2, 2)}) + sel.select(model.index(2, 3), BlockSelectionModel.ClearAndSelect) + self.assertSetEqual(selected(sel), {(2, 2), (2, 3), (3, 2), (3, 3)}) + + self.assertSetEqual(set(sel.selectedItems()), {2, 3}) + sel.setSelectedItems([1, 2]) + self.assertSetEqual(set(sel.selectedItems()), {1, 2}) From af0ec1aa29eae9a8c1a95bf59671e2a4f31fc1a8 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Tue, 12 Jan 2021 09:04:35 +0100 Subject: [PATCH 4/5] owdistancematrix: Preserve model layout in set_labels --- Orange/widgets/unsupervised/owdistancematrix.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Orange/widgets/unsupervised/owdistancematrix.py b/Orange/widgets/unsupervised/owdistancematrix.py index ea84df90b70..ffd6ef62f4b 100644 --- a/Orange/widgets/unsupervised/owdistancematrix.py +++ b/Orange/widgets/unsupervised/owdistancematrix.py @@ -46,7 +46,6 @@ def set_data(self, distances): self.endResetModel() def set_labels(self, labels, variable=None, values=None): - self.beginResetModel() self.labels = labels self.variable = variable self.values = values @@ -55,7 +54,12 @@ def set_labels(self, labels, variable=None, values=None): self.label_colors = variable.palette.values_to_qcolors(values) else: self.label_colors = None - self.endResetModel() + self.headerDataChanged.emit(Qt.Vertical, 0, self.rowCount() - 1) + self.headerDataChanged.emit(Qt.Horizontal, 0, self.columnCount() - 1) + self.dataChanged.emit( + self.index(0, 0), + self.index(self.rowCount() - 1, self.columnCount() - 1) + ) def dimension(self, parent=None): if parent and parent.isValid() or self.distances is None: @@ -291,16 +295,14 @@ def _update_labels(self): var = self.annot_combo.model()[self.annotation_idx] column, _ = self.items.get_column_view(var) labels = [var.str_val(value) for value in column] - saved_selection = self.tableview.selectionModel().selectedIndices() - self.tablemodel.set_labels(labels, var, column) if labels: self.tableview.horizontalHeader().show() self.tableview.verticalHeader().show() else: self.tableview.horizontalHeader().hide() self.tableview.verticalHeader().hide() + self.tablemodel.set_labels(labels, var, column) self.tableview.resizeColumnsToContents() - self.tableview.selectionModel().set_selected_items(saved_selection) def commit(self): sub_table = sub_distances = None From 1b956432a3c0a6caac93fdbe69158f17fbe22b67 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Tue, 12 Jan 2021 09:16:18 +0100 Subject: [PATCH 5/5] itemselectionmodel: Slight cleanup --- Orange/widgets/utils/itemselectionmodel.py | 24 +++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/Orange/widgets/utils/itemselectionmodel.py b/Orange/widgets/utils/itemselectionmodel.py index e23a242635b..2638e29233b 100644 --- a/Orange/widgets/utils/itemselectionmodel.py +++ b/Orange/widgets/utils/itemselectionmodel.py @@ -46,9 +46,6 @@ def select(self, selection: Union[QItemSelection, QModelIndex], model = self.model() - def to_ranges(spans): - return list(range(*r) for r in spans) - if flags & QItemSelectionModel.Current: # no current selection support flags &= ~QItemSelectionModel.Current if flags & QItemSelectionModel.Toggle: # no toggle support either @@ -62,9 +59,8 @@ def to_ranges(spans): selection = QItemSelection() for row_range, col_range in \ product(to_ranges(sel_rows), to_ranges(sel_cols)): - selection.select( - model.index(row_range.start, col_range.start), - model.index(row_range.stop - 1, col_range.stop - 1) + qitemselection_select_range( + selection, model, row_range, col_range ) elif flags & (QItemSelectionModel.Select | QItemSelectionModel.Deselect): @@ -76,15 +72,13 @@ def to_ranges(spans): ext_selection = QItemSelection() for row_range, col_range in \ product(to_ranges(rows), to_ranges(sel_cols)): - ext_selection.select( - model.index(row_range.start, col_range.start), - model.index(row_range.stop - 1, col_range.stop - 1) + qitemselection_select_range( + ext_selection, model, row_range, col_range ) for row_range, col_range in \ product(to_ranges(sel_rows), to_ranges(cols)): - ext_selection.select( - model.index(row_range.start, col_range.start), - model.index(row_range.stop - 1, col_range.stop - 1) + qitemselection_select_range( + ext_selection, model, row_range, col_range ) selection.merge(ext_selection, QItemSelectionModel.Select) super().select(selection, flags) @@ -209,6 +203,10 @@ def qitemselection_select_range( ) +def to_ranges(spans: Iterable[Tuple[int, int]]) -> Sequence[range]: + return list(starmap(range, spans)) + + class SymmetricSelectionModel(QItemSelectionModel): """ Item selection model ensuring the selection is symmetric @@ -216,8 +214,6 @@ class SymmetricSelectionModel(QItemSelectionModel): """ def select(self, selection: Union[QItemSelection, QModelIndex], flags: QItemSelectionModel.SelectionFlags) -> None: - def to_ranges(rngs: Iterable[Tuple[int, int]]) -> Sequence[range]: - return list(starmap(range, rngs)) if isinstance(selection, QModelIndex): selection = QItemSelection(selection, selection)