1from numbers import Number, Integral
2from math import isnan, isinf
3
4import operator
5from collections import namedtuple, defaultdict
6from collections.abc import Sequence
7from contextlib import contextmanager
8from functools import reduce, partial, lru_cache, wraps
9from itertools import chain
10from warnings import warn
11from xml.sax.saxutils import escape
12
13from AnyQt.QtCore import (
14    Qt, QObject, QAbstractListModel, QModelIndex,
15    QItemSelectionModel, QItemSelection)
16from AnyQt.QtCore import pyqtSignal as Signal
17from AnyQt.QtGui import QColor, QBrush
18from AnyQt.QtWidgets import (
19    QWidget, QBoxLayout, QToolButton, QAbstractButton, QAction
20)
21
22import numpy
23
24from orangewidget.utils.itemmodels import (
25    PyListModel, AbstractSortTableModel as _AbstractSortTableModel
26)
27
28from Orange.widgets.utils.colorpalettes import ContinuousPalettes, ContinuousPalette
29from Orange.data import Variable, Storage, DiscreteVariable, ContinuousVariable
30from Orange.data.domain import filter_visible
31from Orange.widgets import gui
32from Orange.widgets.utils import datacaching
33from Orange.statistics import basic_stats
34from Orange.util import deprecated
35
36__all__ = [
37    "PyListModel", "VariableListModel", "PyListModelTooltip", "DomainModel",
38    "AbstractSortTableModel", "PyTableModel", "TableModel",
39    "ModelActionsWidget", "ListSingleSelectionModel"
40]
41
42@contextmanager
43def signal_blocking(obj):
44    blocked = obj.signalsBlocked()
45    obj.blockSignals(True)
46    try:
47        yield
48    finally:
49        obj.blockSignals(blocked)
50
51
52def _as_contiguous_range(the_slice, length):
53    start, stop, step = the_slice.indices(length)
54    if step == -1:
55        # Equivalent range with positive step
56        start, stop, step = stop + 1, start + 1, 1
57    elif not (step == 1 or step is None):
58        raise IndexError("Non-contiguous range.")
59    return start, stop, step
60
61
62class AbstractSortTableModel(_AbstractSortTableModel):
63    # these were defined on TableModel below. When the AbstractSortTableModel
64    # was extracted and made a base of TableModel these deprecations were
65    # misplaced, they belong in TableModel.
66    @deprecated('Orange.widgets.utils.itemmodels.AbstractSortTableModel.mapFromSourceRows')
67    def mapFromTableRows(self, rows):
68        return self.mapFromSourceRows(rows)
69
70    @deprecated('Orange.widgets.utils.itemmodels.AbstractSortTableModel.mapToSourceRows')
71    def mapToTableRows(self, rows):
72        return self.mapToSourceRows(rows)
73
74
75class PyTableModel(AbstractSortTableModel):
76    """ A model for displaying python tables (sequences of sequences) in
77    QTableView objects.
78
79    Parameters
80    ----------
81    sequence : list
82        The initial list to wrap.
83    parent : QObject
84        Parent QObject.
85    editable: bool or sequence
86        If True, all items are flagged editable. If sequence, the True-ish
87        fields mark their respective columns editable.
88
89    Notes
90    -----
91    The model rounds numbers to human readable precision, e.g.:
92    1.23e-04, 1.234, 1234.5, 12345, 1.234e06.
93
94    To set additional item roles, use setData().
95    """
96
97    @staticmethod
98    def _RoleData():
99        return defaultdict(lambda: defaultdict(dict))
100
101    # All methods are either necessary overrides of super methods, or
102    # methods likened to the Python list's. Hence, docstrings aren't.
103    # pylint: disable=missing-docstring
104    def __init__(self, sequence=None, parent=None, editable=False):
105        super().__init__(parent)
106        self._rows = self._cols = 0
107        self._headers = {}
108        self._editable = editable
109        self._table = None
110        self._roleData = {}
111        if sequence is None:
112            sequence = []
113        self.wrap(sequence)
114
115    def rowCount(self, parent=QModelIndex()):
116        return 0 if parent.isValid() else self._rows
117
118    def columnCount(self, parent=QModelIndex()):
119        return 0 if parent.isValid() else self._cols
120
121    def flags(self, index):
122        flags = super().flags(index)
123        if not self._editable or not index.isValid():
124            return flags
125        if isinstance(self._editable, Sequence):
126            return flags | Qt.ItemIsEditable if self._editable[index.column()] else flags
127        return flags | Qt.ItemIsEditable
128
129    def setData(self, index, value, role):
130        row = self.mapFromSourceRows(index.row())
131        if role == Qt.EditRole:
132            self[row][index.column()] = value
133            self.dataChanged.emit(index, index)
134        else:
135            self._roleData[row][index.column()][role] = value
136        return True
137
138    def data(self, index, role=Qt.DisplayRole):
139        if not index.isValid():
140            return
141
142        row, column = self.mapToSourceRows(index.row()), index.column()
143
144        role_value = self._roleData.get(row, {}).get(column, {}).get(role)
145        if role_value is not None:
146            return role_value
147
148        try:
149            value = self[row][column]
150        except IndexError:
151            return
152        if role == Qt.EditRole:
153            return value
154        if role == Qt.DecorationRole and isinstance(value, Variable):
155            return gui.attributeIconDict[value]
156        if role == Qt.DisplayRole:
157            if (isinstance(value, Number) and
158                    not (isnan(value) or isinf(value) or isinstance(value, Integral))):
159                absval = abs(value)
160                strlen = len(str(int(absval)))
161                value = '{:.{}{}}'.format(value,
162                                          2 if absval < .001 else
163                                          3 if strlen < 2 else
164                                          1 if strlen < 5 else
165                                          0 if strlen < 6 else
166                                          3,
167                                          'f' if (absval == 0 or
168                                                  absval >= .001 and
169                                                  strlen < 6)
170                                          else 'e')
171            return str(value)
172        if role == Qt.TextAlignmentRole and isinstance(value, Number):
173            return Qt.AlignRight | Qt.AlignVCenter
174        if role == Qt.ToolTipRole:
175            return str(value)
176
177    def sortColumnData(self, column):
178        return [row[column] for row in self._table]
179
180    def setHorizontalHeaderLabels(self, labels):
181        """
182        Parameters
183        ----------
184        labels : list of str or list of Variable
185        """
186        self._headers[Qt.Horizontal] = tuple(labels)
187
188    def setVerticalHeaderLabels(self, labels):
189        """
190        Parameters
191        ----------
192        labels : list of str or list of Variable
193        """
194        self._headers[Qt.Vertical] = tuple(labels)
195
196    def headerData(self, section, orientation, role=Qt.DisplayRole):
197        headers = self._headers.get(orientation)
198
199        if headers and section < len(headers):
200            section = self.mapToSourceRows(section) if orientation == Qt.Vertical else section
201            value = headers[section]
202
203            if role == Qt.ToolTipRole:
204                role = Qt.DisplayRole
205
206            if role == Qt.DisplayRole:
207                return value.name if isinstance(value, Variable) else value
208
209            if role == Qt.DecorationRole:
210                if isinstance(value, Variable):
211                    return gui.attributeIconDict[value]
212
213        # Use QAbstractItemModel default for non-existent header/sections
214        return super().headerData(section, orientation, role)
215
216    def removeRows(self, row, count, parent=QModelIndex()):
217        if not parent.isValid():
218            del self[row:row + count]
219            for rowidx in range(row, row + count):
220                self._roleData.pop(rowidx, None)
221            self._rows = self._table_dim()[0]
222            return True
223        return False
224
225    def removeColumns(self, column, count, parent=QModelIndex()):
226        self.beginRemoveColumns(parent, column, column + count - 1)
227        for row in self._table:
228            del row[column:column + count]
229        for cols in self._roleData.values():
230            for col in range(column, column + count):
231                cols.pop(col, None)
232        del self._headers.get(Qt.Horizontal, [])[column:column + count]
233        self._cols = self._table_dim()[1]
234        self.endRemoveColumns()
235        return True
236
237    def _table_dim(self):
238        return len(self._table), max(map(len, self), default=0)
239
240    def insertRows(self, row, count, parent=QModelIndex()):
241        self.beginInsertRows(parent, row, row + count - 1)
242        self._table[row:row] = [[''] * self.columnCount() for _ in range(count)]
243        self._rows = self._table_dim()[0]
244        self.endInsertRows()
245        return True
246
247    def insertColumns(self, column, count, parent=QModelIndex()):
248        self.beginInsertColumns(parent, column, column + count - 1)
249        for row in self._table:
250            row[column:column] = [''] * count
251        self._rows = self._table_dim()[0]
252        self.endInsertColumns()
253        return True
254
255    def __len__(self):
256        return len(self._table)
257
258    def __bool__(self):
259        return len(self) != 0
260
261    def __iter__(self):
262        return iter(self._table)
263
264    def __getitem__(self, item):
265        return self._table[item]
266
267    def __delitem__(self, i):
268        if isinstance(i, slice):
269            start, stop, _ = _as_contiguous_range(i, len(self))
270            stop -= 1
271        else:
272            start = stop = i = i if i >= 0 else len(self) + i
273        if stop < start:
274            return
275        self._check_sort_order()
276        self.beginRemoveRows(QModelIndex(), start, stop)
277        del self._table[i]
278        rows = self._table_dim()[0]
279        self._rows = rows
280        self.endRemoveRows()
281        self._update_column_count()
282
283    def __setitem__(self, i, value):
284        self._check_sort_order()
285        if isinstance(i, slice):
286            start, stop, _ = _as_contiguous_range(i, len(self))
287            self.removeRows(start, stop - start)
288            if len(value) == 0:
289                return
290            self.beginInsertRows(QModelIndex(), start, start + len(value) - 1)
291            self._table[start:start] = value
292            self._rows = self._table_dim()[0]
293            self.endInsertRows()
294            self._update_column_count()
295        else:
296            self._table[i] = value
297            self.dataChanged.emit(self.index(i, 0),
298                                  self.index(i, self.columnCount() - 1))
299
300    def _update_column_count(self):
301        cols_before = self._cols
302        cols_after = self._table_dim()[1]
303        if cols_before < cols_after:
304            self.beginInsertColumns(QModelIndex(), cols_before, cols_after - 1)
305            self._cols = cols_after
306            self.endInsertColumns()
307        elif cols_before > cols_after:
308            self.beginRemoveColumns(QModelIndex(), cols_after, cols_before - 1)
309            self._cols = cols_after
310            self.endRemoveColumns()
311
312    def _check_sort_order(self):
313        if self.mapToSourceRows(Ellipsis) is not Ellipsis:
314            warn("Can't modify PyTableModel when it's sorted",
315                 RuntimeWarning, stacklevel=3)
316            raise RuntimeError("Can't modify PyTableModel when it's sorted")
317
318    def wrap(self, table):
319        self.beginResetModel()
320        self._table = table
321        self._roleData = self._RoleData()
322        self._rows, self._cols = self._table_dim()
323        self.resetSorting()
324        self.endResetModel()
325
326    def tolist(self):
327        return self._table
328
329    def clear(self):
330        self.beginResetModel()
331        self._table.clear()
332        self.resetSorting()
333        self._roleData.clear()
334        self._rows, self._cols = self._table_dim()
335        self.endResetModel()
336
337    def append(self, row):
338        self.extend([row])
339
340    def _insertColumns(self, rows):
341        n_max = max(map(len, rows))
342        if self.columnCount() < n_max:
343            self.insertColumns(self.columnCount(), n_max - self.columnCount())
344
345    def extend(self, rows):
346        i, rows = len(self), list(rows)
347        self.insertRows(i, len(rows))
348        self._insertColumns(rows)
349        self[i:] = rows
350
351    def insert(self, i, row):
352        self.insertRows(i, 1)
353        self._insertColumns((row,))
354        self[i] = row
355
356    def remove(self, val):
357        del self[self._table.index(val)]
358
359
360class PyListModelTooltip(PyListModel):
361    def __init__(self):
362        super().__init__()
363        self.tooltips = []
364
365    def data(self, index, role=Qt.DisplayRole):
366        if role == Qt.ToolTipRole:
367            return self.tooltips[index.row()]
368        else:
369            return super().data(index, role)
370
371
372class VariableListModel(PyListModel):
373    MIME_TYPE = "application/x-Orange-VariableList"
374
375    def __init__(self, *args, placeholder=None, **kwargs):
376        super().__init__(*args, **kwargs)
377        self.placeholder = placeholder
378
379    def data(self, index, role=Qt.DisplayRole):
380        if self._is_index_valid(index):
381            var = self[index.row()]
382            if var is None and role == Qt.DisplayRole:
383                return self.placeholder or "None"
384            if not isinstance(var, Variable):
385                return super().data(index, role)
386            elif role == Qt.DisplayRole:
387                return var.name
388            elif role == Qt.DecorationRole:
389                return gui.attributeIconDict[var]
390            elif role == Qt.ToolTipRole:
391                return self.variable_tooltip(var)
392            elif role == gui.TableVariable:
393                return var
394            else:
395                return PyListModel.data(self, index, role)
396
397    def variable_tooltip(self, var):
398        if var.is_discrete:
399            return self.discrete_variable_tooltip(var)
400        elif var.is_time:
401            return self.time_variable_toltip(var)
402        elif var.is_continuous:
403            return self.continuous_variable_toltip(var)
404        elif var.is_string:
405            return self.string_variable_tooltip(var)
406
407    def variable_labels_tooltip(self, var):
408        text = ""
409        if var.attributes:
410            items = [(safe_text(key), safe_text(value))
411                     for key, value in var.attributes.items()]
412            labels = list(map("%s = %s".__mod__, items))
413            text += "<br/>Variable Labels:<br/>"
414            text += "<br/>".join(labels)
415        return text
416
417    def discrete_variable_tooltip(self, var):
418        text = "<b>%s</b><br/>Categorical with %i values: " %\
419               (safe_text(var.name), len(var.values))
420        text += ", ".join("%r" % safe_text(v) for v in var.values)
421        text += self.variable_labels_tooltip(var)
422        return text
423
424    def time_variable_toltip(self, var):
425        text = "<b>%s</b><br/>Time" % safe_text(var.name)
426        text += self.variable_labels_tooltip(var)
427        return text
428
429    def continuous_variable_toltip(self, var):
430        text = "<b>%s</b><br/>Numeric" % safe_text(var.name)
431        text += self.variable_labels_tooltip(var)
432        return text
433
434    def string_variable_tooltip(self, var):
435        text = "<b>%s</b><br/>Text" % safe_text(var.name)
436        text += self.variable_labels_tooltip(var)
437        return text
438
439
440class DomainModel(VariableListModel):
441    ATTRIBUTES, CLASSES, METAS = 1, 2, 4
442    MIXED = ATTRIBUTES | CLASSES | METAS
443    SEPARATED = (CLASSES, PyListModel.Separator,
444                 METAS, PyListModel.Separator,
445                 ATTRIBUTES)
446    PRIMITIVE = (DiscreteVariable, ContinuousVariable)
447
448    def __init__(self, order=SEPARATED, separators=True, placeholder=None,
449                 valid_types=None, alphabetical=False, skip_hidden_vars=True, **kwargs):
450        """
451
452        Parameters
453        ----------
454        order: tuple or int
455            Order of attributes, metas, classes, separators and other options
456        separators: bool
457            If False, remove separators from `order`.
458        placeholder: str
459            The text that is shown when no variable is selected
460        valid_types: tuple
461            (Sub)types of `Variable` that are included in the model
462        alphabetical: bool
463            If true, variables are sorted alphabetically.
464        skip_hidden_vars: bool
465            If true, variables marked as "hidden" are skipped.
466        """
467        super().__init__(placeholder=placeholder, **kwargs)
468        if isinstance(order, int):
469            order = (order,)
470        if placeholder is not None and None not in order:
471            # Add None for the placeholder if it's not already there
472            # Include separator if the current order uses them
473            order = (None,) + \
474                    (self.Separator, ) * (self.Separator in order) + \
475                    order
476        if not separators:
477            order = [e for e in order if e is not self.Separator]
478        self.order = order
479        self.valid_types = valid_types
480        self.alphabetical = alphabetical
481        self.skip_hidden_vars = skip_hidden_vars
482        self._within_set_domain = False
483        self.set_domain(None)
484
485    def set_domain(self, domain):
486        self.beginResetModel()
487        content = []
488        # The logic related to separators is a bit complicated: it ensures that
489        # even when a section is empty we don't have two separators in a row
490        # or a separator at the end
491        add_separator = False
492        for section in self.order:
493            if section is self.Separator:
494                add_separator = True
495                continue
496            if isinstance(section, int):
497                if domain is None:
498                    continue
499                to_add = list(chain(
500                    *(vars for i, vars in enumerate(
501                        (domain.attributes, domain.class_vars, domain.metas))
502                      if (1 << i) & section)))
503                if self.skip_hidden_vars:
504                    to_add = list(filter_visible(to_add))
505                if self.valid_types is not None:
506                    to_add = [var for var in to_add
507                              if isinstance(var, self.valid_types)]
508                if self.alphabetical:
509                    to_add = sorted(to_add, key=lambda x: x.name)
510            elif isinstance(section, list):
511                to_add = section
512            else:
513                to_add = [section]
514            if to_add:
515                if add_separator and content:
516                    content.append(self.Separator)
517                    add_separator = False
518                content += to_add
519        try:
520            self._within_set_domain = True
521            self[:] = content
522        finally:
523            self._within_set_domain = False
524        self.endResetModel()
525
526    def prevent_modification(method):  # pylint: disable=no-self-argument
527        @wraps(method)
528        # pylint: disable=protected-access
529        def e(self, *args, **kwargs):
530            if self._within_set_domain:
531                method(self, *args, **kwargs)
532            else:
533                raise TypeError(
534                    "{} can be modified only by calling 'set_domain'".
535                    format(type(self).__name__))
536        return e
537
538    @prevent_modification
539    def extend(self, iterable):
540        return super().extend(iterable)
541
542    @prevent_modification
543    def append(self, item):
544        return super().append(item)
545
546    @prevent_modification
547    def insert(self, i, val):
548        return super().insert(i, val)
549
550    @prevent_modification
551    def remove(self, val):
552        return super().remove(val)
553
554    @prevent_modification
555    def pop(self, i):
556        return super().pop(i)
557
558    @prevent_modification
559    def clear(self):
560        return super().clear()
561
562    @prevent_modification
563    def __delitem__(self, s):
564        return super().__delitem__(s)
565
566    @prevent_modification
567    def __setitem__(self, s, value):
568        return super().__setitem__(s, value)
569
570    @prevent_modification
571    def reverse(self):
572        return super().reverse()
573
574    @prevent_modification
575    def sort(self, *args, **kwargs):
576        return super().sort(*args, **kwargs)
577
578    def setData(self, index, value, role=Qt.EditRole):
579        # reimplemented
580        if role == Qt.EditRole:
581            return False
582        else:
583            return super().setData(index, value, role)
584
585    def setItemData(self, index, data):
586        # reimplemented
587        if Qt.EditRole in data:
588            return False
589        else:
590            return super().setItemData(index, data)
591
592    def insertRows(self, row, count, parent=QModelIndex()):
593        # reimplemented
594        return False
595
596    def removeRows(self, row, count, parent=QModelIndex()):
597        # reimplemented
598        return False
599
600_html_replace = [("<", "&lt;"), (">", "&gt;")]
601
602
603def safe_text(text):
604    for old, new in _html_replace:
605        text = str(text).replace(old, new)
606    return text
607
608
609class ContinuousPalettesModel(QAbstractListModel):
610    """
611    Model for combo boxes
612    """
613    KeyRole = Qt.UserRole + 1
614    def __init__(self, parent=None, categories=None, icon_width=64):
615        super().__init__(parent)
616        self.icon_width = icon_width
617
618        palettes = list(ContinuousPalettes.values())
619        if categories is None:
620            # Use dict, not set, to keep order of categories
621            categories = dict.fromkeys(palette.category for palette in palettes)
622
623        self.items = []
624        for category in categories:
625            self.items.append(category)
626            self.items += [palette for palette in palettes
627                           if palette.category == category]
628        if len(categories) == 1:
629            del self.items[0]
630
631    def rowCount(self, parent):
632        return 0 if parent.isValid() else len(self.items)
633
634    @staticmethod
635    def columnCount(parent):
636        return 0 if parent.isValid() else 1
637
638    def data(self, index, role):
639        item = self.items[index.row()]
640        if isinstance(item, str):
641            if role in [Qt.EditRole, Qt.DisplayRole]:
642                return item
643        else:
644            if role in [Qt.EditRole, Qt.DisplayRole]:
645                return item.friendly_name
646            if role == Qt.DecorationRole:
647                return item.color_strip(self.icon_width, 16)
648            if role == Qt.UserRole:
649                return item
650            if role == self.KeyRole:
651                return item.name
652        return None
653
654    def flags(self, index):
655        item = self.items[index.row()]
656        if isinstance(item, ContinuousPalette):
657            return Qt.ItemIsEnabled | Qt.ItemIsSelectable
658        else:
659            return Qt.NoItemFlags
660
661    def indexOf(self, x):
662        if isinstance(x, str):
663            for i, item in enumerate(self.items):
664                if not isinstance(item, str) \
665                        and x in (item.name, item.friendly_name):
666                    return i
667        elif isinstance(x, ContinuousPalette):
668            return self.items.index(x)
669        return None
670
671
672class ListSingleSelectionModel(QItemSelectionModel):
673    """ Item selection model for list item models with single selection.
674
675    Defines signal:
676        - selectedIndexChanged(QModelIndex)
677
678    """
679    selectedIndexChanged = Signal(QModelIndex)
680
681    def __init__(self, model, parent=None):
682        QItemSelectionModel.__init__(self, model, parent)
683        self.selectionChanged.connect(self.onSelectionChanged)
684
685    def onSelectionChanged(self, new, _):
686        index = list(new.indexes())
687        if index:
688            index = index.pop()
689        else:
690            index = QModelIndex()
691
692        self.selectedIndexChanged.emit(index)
693
694    def selectedRow(self):
695        """ Return QModelIndex of the selected row or invalid if no selection.
696        """
697        rows = self.selectedRows()
698        if rows:
699            return rows[0]
700        else:
701            return QModelIndex()
702
703    def select(self, index, flags=QItemSelectionModel.ClearAndSelect):
704        if isinstance(index, int):
705            index = self.model().index(index)
706        return QItemSelectionModel.select(self, index, flags)
707
708
709def select_row(view, row):
710    """
711    Select a `row` in an item view.
712    """
713    selmodel = view.selectionModel()
714    selmodel.select(view.model().index(row, 0),
715                    QItemSelectionModel.ClearAndSelect |
716                    QItemSelectionModel.Rows)
717
718
719def select_rows(view, row_indices, command=QItemSelectionModel.ClearAndSelect):
720    """
721    Select several rows in view.
722
723    :param QAbstractItemView view:
724    :param row_indices: Integer indices of rows to select.
725    :param command: QItemSelectionModel.SelectionFlags
726    """
727    selmodel = view.selectionModel()
728    model = view.model()
729    selection = QItemSelection()
730    for row in row_indices:
731        index = model.index(row, 0)
732        selection.select(index, index)
733    selmodel.select(selection, command | QItemSelectionModel.Rows)
734
735
736class ModelActionsWidget(QWidget):
737    def __init__(self, actions=None, parent=None,
738                 direction=QBoxLayout.LeftToRight):
739        QWidget.__init__(self, parent)
740        self.actions = []
741        self.buttons = []
742        layout = QBoxLayout(direction)
743        layout.setContentsMargins(0, 0, 0, 0)
744        self.setContentsMargins(0, 0, 0, 0)
745        self.setLayout(layout)
746        if actions is not None:
747            for action in actions:
748                self.addAction(action)
749        self.setLayout(layout)
750
751    def actionButton(self, action):
752        if isinstance(action, QAction):
753            button = QToolButton(self)
754            button.setDefaultAction(action)
755            return button
756        elif isinstance(action, QAbstractButton):
757            return action
758
759    def insertAction(self, ind, action, *args):
760        button = self.actionButton(action)
761        self.layout().insertWidget(ind, button, *args)
762        self.buttons.insert(ind, button)
763        self.actions.insert(ind, action)
764        return button
765
766    def addAction(self, action, *args):
767        return self.insertAction(-1, action, *args)
768
769
770class TableModel(AbstractSortTableModel):
771    """
772    An adapter for using Orange.data.Table within Qt's Item View Framework.
773
774    :param Orange.data.Table sourcedata: Source data table.
775    :param QObject parent:
776    """
777    #: Orange.data.Value for the index.
778    ValueRole = gui.TableValueRole  # next(gui.OrangeUserRole)
779    #: Orange.data.Value of the row's class.
780    ClassValueRole = gui.TableClassValueRole  # next(gui.OrangeUserRole)
781    #: Orange.data.Variable of the column.
782    VariableRole = gui.TableVariable  # next(gui.OrangeUserRole)
783    #: Basic statistics of the column
784    VariableStatsRole = next(gui.OrangeUserRole)
785    #: The column's role (position) in the domain.
786    #: One of Attribute, ClassVar or Meta
787    DomainRole = next(gui.OrangeUserRole)
788
789    #: Column domain roles
790    ClassVar, Meta, Attribute = range(3)
791
792    #: Default background color for domain roles
793    ColorForRole = {
794        ClassVar: QColor(160, 160, 160),
795        Meta: QColor(220, 220, 200),
796        Attribute: None,
797    }
798
799    #: Standard column descriptor
800    Column = namedtuple(
801        "Column", ["var", "role", "background", "format"])
802    #: Basket column descriptor (i.e. sparse X/Y/metas/ compressed into
803    #: a single column).
804    Basket = namedtuple(
805        "Basket", ["vars", "role", "background", "density", "format"])
806
807    # The class uses the same names (X_density etc) as Table
808    # pylint: disable=invalid-name
809    def __init__(self, sourcedata, parent=None):
810        super().__init__(parent)
811        self.source = sourcedata
812        self.domain = domain = sourcedata.domain
813
814        self.X_density = sourcedata.X_density()
815        self.Y_density = sourcedata.Y_density()
816        self.M_density = sourcedata.metas_density()
817
818        brush_for_role = {
819            role: QBrush(c) if c is not None else None
820            for role, c in self.ColorForRole.items()
821        }
822
823        def format_sparse(vars, datagetter, instance):
824            data = datagetter(instance)
825            return ", ".join("{}={}".format(vars[i].name, vars[i].repr_val(v))
826                             for i, v in zip(data.indices, data.data))
827
828        def format_sparse_bool(vars, datagetter, instance):
829            data = datagetter(instance)
830            return ", ".join(vars[i].name for i in data.indices)
831
832        def format_dense(var, instance):
833            return str(instance[var])
834
835        def make_basket_formater(vars, density, role):
836            formater = (format_sparse if density == Storage.SPARSE
837                        else format_sparse_bool)
838            if role == TableModel.Attribute:
839                getter = operator.attrgetter("sparse_x")
840            elif role == TableModel.ClassVar:
841                getter = operator.attrgetter("sparse_y")
842            elif role == TableModel.Meta:
843                getter = operator.attrgetter("sparse_metas")
844            return partial(formater, vars, getter)
845
846        def make_basket(vars, density, role):
847            return TableModel.Basket(
848                vars, TableModel.Attribute, brush_for_role[role], density,
849                make_basket_formater(vars, density, role)
850            )
851
852        def make_column(var, role):
853            return TableModel.Column(
854                var, role, brush_for_role[role],
855                partial(format_dense, var)
856            )
857
858        columns = []
859
860        if self.Y_density != Storage.DENSE and domain.class_vars:
861            coldesc = make_basket(domain.class_vars, self.Y_density,
862                                  TableModel.ClassVar)
863            columns.append(coldesc)
864        else:
865            columns += [make_column(var, TableModel.ClassVar)
866                        for var in domain.class_vars]
867
868        if self.M_density != Storage.DENSE and domain.metas:
869            coldesc = make_basket(domain.metas, self.M_density,
870                                  TableModel.Meta)
871            columns.append(coldesc)
872        else:
873            columns += [make_column(var, TableModel.Meta)
874                        for var in domain.metas]
875
876        if self.X_density != Storage.DENSE and domain.attributes:
877            coldesc = make_basket(domain.attributes, self.X_density,
878                                  TableModel.Attribute)
879            columns.append(coldesc)
880        else:
881            columns += [make_column(var, TableModel.Attribute)
882                        for var in domain.attributes]
883
884        #: list of all domain variables (class_vars + metas + attrs)
885        self.vars = domain.class_vars + domain.metas + domain.attributes
886        self.columns = columns
887
888        #: A list of all unique attribute labels (in all variables)
889        self._labels = sorted(
890            reduce(operator.ior,
891                   [set(var.attributes) for var in self.vars],
892                   set()))
893
894        @lru_cache(maxsize=1000)
895        def row_instance(index):
896            return self.source[int(index)]
897        self._row_instance = row_instance
898
899        # column basic statistics (VariableStatsRole), computed when
900        # first needed.
901        self.__stats = None
902        self.__rowCount = sourcedata.approx_len()
903        self.__columnCount = len(self.columns)
904
905        if self.__rowCount > (2 ** 31 - 1):
906            raise ValueError("len(sourcedata) > 2 ** 31 - 1")
907
908    def sortColumnData(self, column):
909        return self._columnSortKeyData(column, TableModel.ValueRole)
910
911    @deprecated('Orange.widgets.utils.itemmodels.TableModel.sortColumnData')
912    def columnSortKeyData(self, column, role):
913        return self._columnSortKeyData(column, role)
914
915    def _columnSortKeyData(self, column, role):
916        """
917        Return a sequence of source table objects which can be used as
918        `keys` for sorting.
919
920        :param int column: Sort column.
921        :param Qt.ItemRole role: Sort item role.
922
923        """
924        coldesc = self.columns[column]
925        if isinstance(coldesc, TableModel.Column) \
926                and role == TableModel.ValueRole:
927            col_data = numpy.asarray(self.source.get_column_view(coldesc.var)[0])
928
929            if coldesc.var.is_continuous:
930                # continuous from metas have dtype object; cast it to float
931                col_data = col_data.astype(float)
932            return col_data
933        else:
934            return numpy.asarray([self.index(i, column).data(role)
935                                  for i in range(self.rowCount())])
936
937    def data(self, index, role,
938             # For optimizing out LOAD_GLOBAL byte code instructions in
939             # the item role tests.
940             _str=str,
941             _Qt_DisplayRole=Qt.DisplayRole,
942             _Qt_EditRole=Qt.EditRole,
943             _Qt_BackgroundRole=Qt.BackgroundRole,
944             _ValueRole=ValueRole,
945             _ClassValueRole=ClassValueRole,
946             _VariableRole=VariableRole,
947             _DomainRole=DomainRole,
948             _VariableStatsRole=VariableStatsRole,
949             # Some cached local precomputed values.
950             # All of the above roles we respond to
951             _recognizedRoles=frozenset([Qt.DisplayRole,
952                                         Qt.EditRole,
953                                         Qt.BackgroundRole,
954                                         ValueRole,
955                                         ClassValueRole,
956                                         VariableRole,
957                                         DomainRole,
958                                         VariableStatsRole])):
959        """
960        Reimplemented from `QAbstractItemModel.data`
961        """
962        if role not in _recognizedRoles:
963            return None
964
965        row, col = index.row(), index.column()
966        if  not 0 <= row <= self.__rowCount:
967            return None
968
969        row = self.mapToSourceRows(row)
970
971        try:
972            instance = self._row_instance(row)
973        except IndexError:
974            self.layoutAboutToBeChanged.emit()
975            self.beginRemoveRows(self.parent(), row, max(self.rowCount(), row))
976            self.__rowCount = min(row, self.__rowCount)
977            self.endRemoveRows()
978            self.layoutChanged.emit()
979            return None
980        coldesc = self.columns[col]
981
982        if role == _Qt_DisplayRole:
983            return coldesc.format(instance)
984        elif role == _Qt_EditRole and isinstance(coldesc, TableModel.Column):
985            return instance[coldesc.var]
986        elif role == _Qt_BackgroundRole:
987            return coldesc.background
988        elif role == _ValueRole and isinstance(coldesc, TableModel.Column):
989            return instance[coldesc.var]
990        elif role == _ClassValueRole:
991            try:
992                return instance.get_class()
993            except TypeError:
994                return None
995        elif role == _VariableRole and isinstance(coldesc, TableModel.Column):
996            return coldesc.var
997        elif role == _DomainRole:
998            return coldesc.role
999        elif role == _VariableStatsRole:
1000            return self._stats_for_column(col)
1001        else:
1002            return None
1003
1004    def setData(self, index, value, role):
1005        row = self.mapFromSourceRows(index.row())
1006        if role == Qt.EditRole:
1007            try:
1008                self.source[row, index.column()] = value
1009            except (TypeError, IndexError):
1010                return False
1011            else:
1012                self.dataChanged.emit(index, index)
1013                return True
1014        else:
1015            return False
1016
1017    def parent(self, index=QModelIndex()):
1018        """Reimplemented from `QAbstractTableModel.parent`."""
1019        return QModelIndex()
1020
1021    def rowCount(self, parent=QModelIndex()):
1022        """Reimplemented from `QAbstractTableModel.rowCount`."""
1023        return 0 if parent.isValid() else self.__rowCount
1024
1025    def columnCount(self, parent=QModelIndex()):
1026        """Reimplemented from `QAbstractTableModel.columnCount`."""
1027        return 0 if parent.isValid() else self.__columnCount
1028
1029    def headerData(self, section, orientation, role):
1030        """Reimplemented from `QAbstractTableModel.headerData`."""
1031        if orientation == Qt.Vertical:
1032            if role == Qt.DisplayRole:
1033                return int(self.mapToSourceRows(section) + 1)
1034            return None
1035
1036        coldesc = self.columns[section]
1037        if role == Qt.DisplayRole:
1038            if isinstance(coldesc, TableModel.Basket):
1039                return "{...}"
1040            else:
1041                return coldesc.var.name
1042        elif role == Qt.ToolTipRole:
1043            return self._tooltip(coldesc)
1044        elif role == TableModel.VariableRole \
1045                and isinstance(coldesc, TableModel.Column):
1046            return coldesc.var
1047        elif role == TableModel.VariableStatsRole:
1048            return self._stats_for_column(section)
1049        elif role == TableModel.DomainRole:
1050            return coldesc.role
1051        else:
1052            return None
1053
1054    def _tooltip(self, coldesc):
1055        """
1056        Return an header tool tip text for an `column` descriptor.
1057        """
1058        if isinstance(coldesc, TableModel.Basket):
1059            return None
1060
1061        labels = self._labels
1062        variable = coldesc.var
1063        pairs = [(escape(key), escape(str(variable.attributes[key])))
1064                 for key in labels if key in variable.attributes]
1065        tip = "<b>%s</b>" % escape(variable.name)
1066        tip = "<br/>".join([tip] + ["%s = %s" % pair for pair in pairs])
1067        return tip
1068
1069    def _stats_for_column(self, column):
1070        """
1071        Return BasicStats for `column` index.
1072        """
1073        coldesc = self.columns[column]
1074        if isinstance(coldesc, TableModel.Basket):
1075            return None
1076
1077        if self.__stats is None:
1078            self.__stats = datacaching.getCached(
1079                self.source, basic_stats.DomainBasicStats,
1080                (self.source, True)
1081            )
1082
1083        return self.__stats[coldesc.var]
1084