Skip to content

Commit

Permalink
Merge pull request #5176 from ales-erjavec/distance-matrix-selection-…
Browse files Browse the repository at this point in the history
…model

[FIX] Distance Matrix: Fix freeze with large selections
  • Loading branch information
janezd authored Jan 22, 2021
2 parents 62c724c + 1b95643 commit 5236e3b
Show file tree
Hide file tree
Showing 4 changed files with 327 additions and 210 deletions.
163 changes: 3 additions & 160 deletions Orange/widgets/data/owtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import concurrent.futures

from collections import OrderedDict, namedtuple
from typing import List, Tuple, Iterable

from math import isnan

Expand All @@ -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
Expand Down Expand Up @@ -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.
"""
Expand Down
63 changes: 13 additions & 50 deletions Orange/widgets/unsupervised/owdistancematrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -47,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
Expand All @@ -56,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:
Expand Down Expand Up @@ -121,44 +124,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()
Expand Down Expand Up @@ -207,12 +172,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):
Expand Down Expand Up @@ -284,7 +249,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"]
Expand Down Expand Up @@ -330,21 +295,19 @@ 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()
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
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):
Expand Down
Loading

0 comments on commit 5236e3b

Please sign in to comment.