Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] Distance Matrix: Fix freeze with large selections #5176

Merged
merged 7 commits into from
Jan 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 7 additions & 199 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 @@ -17,12 +16,12 @@
QTableView, QHeaderView, QAbstractButton, QApplication, QStyleOptionHeader,
QStyle, QStylePainter, QStyledItemDelegate
)
from AnyQt.QtGui import QColor, QClipboard, QMouseEvent
from AnyQt.QtGui import QColor, QClipboard
from AnyQt.QtCore import (
Qt, QSize, QEvent, QByteArray, QMimeData, QObject, QMetaObject,
QAbstractProxyModel, QIdentityProxyModel, QModelIndex,
QItemSelectionModel, QItemSelection, QItemSelectionRange,
Signal)
)
from AnyQt.QtCore import pyqtSlot as Slot

import Orange.data
Expand All @@ -33,6 +32,10 @@

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
from Orange.widgets.utils import datacaching
Expand Down Expand Up @@ -160,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 Expand Up @@ -363,42 +207,7 @@ def table_selection_to_list(table):
TableSlot = namedtuple("TableSlot", ["input_id", "table", "summary", "view"])


class TableView(gui.HScrollStepMixin, QTableView):
#: Signal emitted when selection finished. It is not emitted during
#: mouse drag selection updates.
selectionFinished = Signal()

__mouseDown = False
__selectionDidChange = False

def setSelectionModel(self, selectionModel: QItemSelectionModel) -> None:
sm = self.selectionModel()
if sm is not None:
sm.selectionChanged.disconnect(self.__on_selectionChanged)
super().setSelectionModel(selectionModel)
if selectionModel is not None:
selectionModel.selectionChanged.connect(self.__on_selectionChanged)

def __on_selectionChanged(self):
if self.__mouseDown:
self.__selectionDidChange = True
else:
self.selectionFinished.emit()

def mousePressEvent(self, event: QMouseEvent) -> None:
self.__mouseDown = event.button() == Qt.LeftButton
super().mousePressEvent(event)

def mouseReleaseEvent(self, event: QMouseEvent) -> None:
super().mouseReleaseEvent(event)
if self.__mouseDown and event.button() == Qt.LeftButton:
self.__mouseDown = False
if self.__selectionDidChange:
self.__selectionDidChange = False
self.selectionFinished.emit()


class DataTableView(TableView):
class DataTableView(gui.HScrollStepMixin, TableView):
dataset: Table
input_slot: TableSlot

Expand Down Expand Up @@ -500,7 +309,6 @@ def set_dataset(self, data, tid=None):
else:
view = DataTableView()
view.setSortingEnabled(True)
view.setHorizontalScrollMode(QTableView.ScrollPerPixel)

if self.select_rows:
view.setSelectionBehavior(QTableView.SelectRows)
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