From 13461ccc850fe96e66a6487ed27525a91d49f05f Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Mon, 5 Feb 2018 15:52:15 +0100 Subject: [PATCH 1/3] owtable: Optimize selection in the view --- Orange/widgets/data/owtable.py | 137 +++++++++++++++++++++++---------- 1 file changed, 96 insertions(+), 41 deletions(-) diff --git a/Orange/widgets/data/owtable.py b/Orange/widgets/data/owtable.py index 3c1f6e40e35..9f6ea628287 100644 --- a/Orange/widgets/data/owtable.py +++ b/Orange/widgets/data/owtable.py @@ -6,6 +6,8 @@ import concurrent.futures from collections import OrderedDict, namedtuple +from typing import List, Tuple, Iterable # pylint: disable=unused-import + from math import isnan import numpy @@ -235,55 +237,54 @@ def select(self, selection, flags): if isinstance(selection, QModelIndex): selection = QItemSelection(selection, selection) - model = self.model() - indexes = self.selectedIndexes() - - rows = set(ind.row() for ind in indexes) - cols = set(ind.column() for ind in indexes) - - if flags & QItemSelectionModel.Select and \ - not flags & QItemSelectionModel.Clear and self.__selectBlocks: - indexes = selection.indexes() - sel_rows = set(ind.row() for ind in indexes).union(rows) - sel_cols = set(ind.column() for ind in indexes).union(cols) - - selection = QItemSelection() - - for r_start, r_end in ranges(sorted(sel_rows)): - for c_start, c_end in ranges(sorted(sel_cols)): - top_left = model.index(r_start, c_start) - bottom_right = model.index(r_end - 1, c_end - 1) - selection.select(top_left, bottom_right) - elif self.__selectBlocks and flags & QItemSelectionModel.Deselect: - indexes = selection.indexes() + if not self.__selectBlocks: + super().select(selection, flags) + return - def to_ranges(indices): - return list(range(*r) for r in ranges(indices)) + model = self.model() - selected_rows = to_ranges(sorted(rows)) - selected_cols = to_ranges(sorted(cols)) + def to_ranges(spans): + return list(range(*r) for r in spans) - desel_rows = to_ranges(set(ind.row() for ind in indexes)) - desel_cols = to_ranges(set(ind.column() for ind in indexes)) + 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() - - # deselection extended vertically for row_range, col_range in \ - itertools.product(selected_rows, desel_cols): + 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) ) - # deselection extended horizontally + 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(desel_rows, selected_cols): - selection.select( + 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) ) - - QItemSelectionModel.select(self, selection, flags) + 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.""" @@ -299,7 +300,59 @@ def setSelectBlocks(self, state): 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'. @@ -764,24 +817,26 @@ def get_selection(self, view): """ Return the selected row and column indices of the selection in view. """ - selection = view.selectionModel().selection() + selmodel = view.selectionModel() + + selection = selmodel.selection() model = view.model() # map through the proxies into input table. while isinstance(model, QAbstractProxyModel): selection = model.mapSelectionToSource(selection) model = model.sourceModel() + assert isinstance(selmodel, BlockSelectionModel) assert isinstance(model, TableModel) - indexes = selection.indexes() - - rows = numpy.unique([ind.row() for ind in indexes]) + row_spans, col_spans = selection_blocks(selection) + rows = list(itertools.chain.from_iterable(itertools.starmap(range, row_spans))) + cols = list(itertools.chain.from_iterable(itertools.starmap(range, col_spans))) + rows = numpy.array(rows, dtype=numpy.intp) # map the rows through the applied sorting (if any) rows = model.mapToSourceRows(rows) rows.sort() rows = rows.tolist() - - cols = sorted(set(ind.column() for ind in indexes)) return rows, cols @staticmethod From b5e6150dd130ddd5489be836dbb3abc5fb9e90d2 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Tue, 6 Feb 2018 11:45:50 +0100 Subject: [PATCH 2/3] owtable: Remove one model proxying layer Transform RichTableDecorator to RichTableModel, a subclass of TableModel, to minimize proxying overhead. --- Orange/widgets/data/owtable.py | 93 ++++++++-------------------------- 1 file changed, 22 insertions(+), 71 deletions(-) diff --git a/Orange/widgets/data/owtable.py b/Orange/widgets/data/owtable.py index 9f6ea628287..5a741960311 100644 --- a/Orange/widgets/data/owtable.py +++ b/Orange/widgets/data/owtable.py @@ -42,8 +42,8 @@ from Orange.widgets.utils.itemmodels import TableModel -class RichTableDecorator(QIdentityProxyModel): - """A proxy model for a TableModel with some bells and whistles +class RichTableModel(TableModel): + """A TableModel with some extra bells and whistles/ (adds support for gui.BarRole, include variable labels and icons in the header) @@ -51,41 +51,17 @@ class RichTableDecorator(QIdentityProxyModel): #: Rich header data flags. Name, Labels, Icon = 1, 2, 4 - def __init__(self, source, parent=None): - super().__init__(parent) + def __init__(self, sourcedata, parent=None): + super().__init__(sourcedata, parent) - self._header_flags = RichTableDecorator.Name - self._labels = [] - self._continuous = [] - - self.setSourceModel(source) - - @property - def source(self): - return getattr(self.sourceModel(), "source", None) - - @property - def vars(self): - return getattr(self.sourceModel(), "vars", []) - - def setSourceModel(self, source): - if source is not None and \ - not isinstance(source, TableModel): - raise TypeError() - - if source is not None: - self._continuous = [var.is_continuous for var in source.vars] - labels = [] - for var in source.vars: - if isinstance(var, Orange.data.Variable): - labels.extend(var.attributes.keys()) - self._labels = list(sorted( - {label for label in labels if not label.startswith("_")})) - else: - self._continuous = [] - self._labels = [] - - super().setSourceModel(source) + self._header_flags = RichTableModel.Name + self._continuous = [var.is_continuous for var in self.vars] + labels = [] + for var in self.vars: + if isinstance(var, Orange.data.Variable): + labels.extend(var.attributes.keys()) + self._labels = list(sorted( + {label for label in labels if not label.startswith("_")})) def data(self, index, role=Qt.DisplayRole, # for faster local lookup @@ -106,36 +82,30 @@ def data(self, index, role=Qt.DisplayRole, return super().data(index, role) def headerData(self, section, orientation, role): - if self.sourceModel() is None: - return None - - # NOTE: Always use `self.sourceModel().heaerData(...)` and not - # super().headerData(...). The later does not work for zero length - # source models if orientation == Qt.Horizontal and role == Qt.DisplayRole: - var = self.sourceModel().headerData( + var = super().headerData( section, orientation, TableModel.VariableRole) if var is None: - return self.sourceModel().headerData( + return super().headerData( section, orientation, Qt.DisplayRole) lines = [] - if self._header_flags & RichTableDecorator.Name: + if self._header_flags & RichTableModel.Name: lines.append(var.name) - if self._header_flags & RichTableDecorator.Labels: + if self._header_flags & RichTableModel.Labels: lines.extend(str(var.attributes.get(label, "")) for label in self._labels) return "\n".join(lines) elif orientation == Qt.Horizontal and role == Qt.DecorationRole and \ - self._header_flags & RichTableDecorator.Icon: - var = self.sourceModel().headerData( + self._header_flags & RichTableModel.Icon: + var = super().headerData( section, orientation, TableModel.VariableRole) if var is not None: return gui.attributeIconDict[var] else: return None else: - return self.sourceModel().headerData(section, orientation, role) + return super().headerData(section, orientation, role) def setRichHeaderFlags(self, flags): if flags != self._header_flags: @@ -146,24 +116,6 @@ def setRichHeaderFlags(self, flags): def richHeaderFlags(self): return self._header_flags - if QT_VERSION < 0xFFFFFF: # TODO: change when QTBUG-44143 is fixed - def sort(self, column, order): - # Preempt the layout change notification - self.layoutAboutToBeChanged.emit() - # Block signals to suppress repeated layout[AboutToBe]Changed - # TODO: Are any other signals emitted during a sort? - self.blockSignals(True) - try: - rval = self.sourceModel().sort(column, order) - finally: - self.blockSignals(False) - # Tidy up. - self.layoutChanged.emit() - return rval - else: - def sort(self, column, order): - return self.sourceModel().sort(column, order) - class TableSliceProxy(QIdentityProxyModel): def __init__(self, parent=None, rowSlice=slice(0, -1), **kwargs): @@ -574,8 +526,7 @@ def _setup_table_view(self, view, data): view.setModel(None) return - datamodel = TableModel(data) - datamodel = RichTableDecorator(datamodel) + datamodel = RichTableModel(data) rowcount = data.approx_len() @@ -707,7 +658,7 @@ def _update_variable_labels(self, view): if self.show_attribute_labels: model.setRichHeaderFlags( - RichTableDecorator.Labels | RichTableDecorator.Name) + RichTableModel.Labels | RichTableModel.Name) labelnames = set() for a in model.source.domain.variables: @@ -716,7 +667,7 @@ def _update_variable_labels(self, view): [label for label in labelnames if not label.startswith("_")]) self.set_corner_text(view, "\n".join([""] + labelnames)) else: - model.setRichHeaderFlags(RichTableDecorator.Name) + model.setRichHeaderFlags(RichTableModel.Name) self.set_corner_text(view, "") def _on_show_variable_labels_changed(self): From 226d1dae474f0be00dc3ad8807fed0f2c573efdc Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Tue, 6 Feb 2018 15:02:18 +0100 Subject: [PATCH 3/3] AbstractSortTableModel: Use Qt 5 flags for layout change hinting --- Orange/widgets/utils/itemmodels.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Orange/widgets/utils/itemmodels.py b/Orange/widgets/utils/itemmodels.py index fd23555fdc4..019dd25169d 100644 --- a/Orange/widgets/utils/itemmodels.py +++ b/Orange/widgets/utils/itemmodels.py @@ -11,7 +11,7 @@ from AnyQt.QtCore import ( Qt, QObject, QAbstractListModel, QAbstractTableModel, QModelIndex, - QItemSelectionModel + QItemSelectionModel, QT_VERSION ) from AnyQt.QtCore import pyqtSignal as Signal from AnyQt.QtGui import QColor @@ -197,7 +197,12 @@ def sort(self, column: int, order: Qt.SortOrder = Qt.AscendingOrder): data table is left unmodified. Use mapToSourceRows()/mapFromSourceRows() when accessing data by row indexes. """ - self.layoutAboutToBeChanged.emit() + if QT_VERSION >= 0x50000: + self.layoutAboutToBeChanged.emit( + [], QAbstractTableModel.VerticalSortHint + ) + else: + self.layoutAboutToBeChanged.emit() # Store persistent indices as well as their (actual) rows in the # source data table. @@ -230,7 +235,10 @@ def sort(self, column: int, order: Qt.SortOrder = Qt.AscendingOrder): persistent, [self.index(row, pind.column()) for row, pind in zip(persistent_rows, persistent)]) - self.layoutChanged.emit() + if QT_VERSION >= 0x50000: + self.layoutChanged.emit([], QAbstractTableModel.VerticalSortHint) + else: + self.layoutChanged.emit() class PyTableModel(AbstractSortTableModel):