source: orange/Orange/OrangeWidgets/OWItemModels.py @ 11738:6fc96a528c93

Revision 11738:6fc96a528c93, 15.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Cleanup OWItemModels

RevLine 
[11738]1
2import cPickle
[9235]3
[11737]4from xml.sax.saxutils import escape
[11738]5from functools import wraps, partial
6from collections import defaultdict
7from contextlib import contextmanager
[11737]8
[8042]9from PyQt4.QtCore import *
10from PyQt4.QtGui import *
11
[11738]12import OWGUI
13import Orange
14
[8042]15
[9117]16class _store(dict):
17    pass
18
19def _argsort(seq, cmp=None, key=None, reverse=False):
20    if key is not None:
21        items = sorted(zip(range(len(seq)), seq), key=lambda i,v: key(v))
22    elif cmp is not None:
23        items = sorted(zip(range(len(seq)), seq), cmp=lambda a,b: cmp(a[1], b[1]))
24    else:
25        items = sorted(zip(range(len(seq)), seq), key=seq.__getitem__)
26    if reverse:
27        items = reversed(items)
28    return items
29
[9180]30
31@contextmanager
32def signal_blocking(object):
33    blocked = object.signalsBlocked()
34    object.blockSignals(True)
35    yield
36    object.blockSignals(blocked)
37
[10725]38
[8042]39class PyListModel(QAbstractListModel):
40    """ A model for displaying python list like objects in Qt item view classes
41    """
[9180]42    MIME_TYPES = ["application/x-Orange-PyListModelData"]
[10725]43
[9117]44    def __init__(self, iterable=[], parent=None,
45                 flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,
46                 list_item_role=Qt.DisplayRole,
47                 supportedDropActions=Qt.MoveAction):
[8042]48        QAbstractListModel.__init__(self, parent)
49        self._list = []
[9117]50        self._other_data = []
51        self._flags = flags
52        self.list_item_role = list_item_role
[10725]53
[9117]54        self._supportedDropActions = supportedDropActions
[8042]55        self.extend(iterable)
[10725]56
57    def _is_index_valid_for(self, index, list_like):
58        if isinstance(index, QModelIndex) and index.isValid() :
59            row, column = index.row(), index.column()
60            return row < len(list_like) and row >= 0 and column == 0
61        elif isinstance(index, int):
62            return index < len(list_like) and index > -len(self)
63        else:
64            return False
65
[8042]66    def wrap(self, list):
67        """ Wrap the list with this model. All changes to the model
68        are done in place on the passed list
69        """
70        self._list = list
[9117]71        self._other_data = [_store() for _ in list]
[8042]72        self.reset()
[10725]73
[8042]74    def index(self, row, column=0, parent=QModelIndex()):
[10725]75        if self._is_index_valid_for(row, self) and column == 0:
76            return QAbstractListModel.createIndex(self, row, column, parent)
77        else:
78            return QModelIndex()
79
[8042]80    def headerData(self, section, orientation, role=Qt.DisplayRole):
81        if role == Qt.DisplayRole:
82            return QVariant(str(section))
[10725]83
[8042]84    def rowCount(self, parent=QModelIndex()):
85        return 0 if parent.isValid() else len(self)
[10725]86
[8042]87    def columnCount(self, parent=QModelIndex()):
88        return 0 if parent.isValid() else 1
[10725]89
[8042]90    def data(self, index, role=Qt.DisplayRole):
91        row = index.row()
[10725]92        if role in [self.list_item_role, Qt.EditRole] \
93                and self._is_index_valid_for(index, self):
94            return QVariant(self[row])
95        elif self._is_index_valid_for(row, self._other_data):
96            return QVariant(self._other_data[row].get(role, QVariant()))
[9117]97        else:
[10725]98            return QVariant()
99
[9117]100    def itemData(self, index):
101        map = QAbstractListModel.itemData(self, index)
[10725]102        if self._is_index_valid_for(index, self._other_data):
103            items = self._other_data[index.row()].items()
104        else:
105            items = []
106
107        for key, value in items:
[9117]108            map[key] = QVariant(value)
[10725]109
[9117]110        return map
[10725]111
[8042]112    def parent(self, index=QModelIndex()):
113        return QModelIndex()
[10725]114
[8042]115    def setData(self, index, value, role=Qt.EditRole):
[10725]116        if role == Qt.EditRole and self._is_index_valid_for(index, self):
[9117]117            obj = value.toPyObject()
[10725]118            self[index.row()] = obj # Will emit proper dataChanged signal
119            return True
120        elif self._is_index_valid_for(index, self._other_data):
[9117]121            self._other_data[index.row()][role] = value
122            self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
[10725]123            return True
124        else:
125            return False
126
[9180]127    def setItemData(self, index, data):
128        data = dict(data)
129        with signal_blocking(self):
130            for role, value in data.items():
[10725]131                if role == Qt.EditRole and \
132                        self._is_index_valid_for(index, self):
[9180]133                    self[index.row()] = value.toPyObject()
[10725]134                elif self._is_index_valid_for(index, self._other_data):
[9180]135                    self._other_data[index.row()][role] = value
[10725]136
[9180]137        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
138        return True
[10725]139
[8042]140    def flags(self, index):
[10725]141        if self._is_index_valid_for(index, self._other_data):
[9117]142            return self._other_data[index.row()].get("flags", self._flags)
143        else:
144            return self._flags | Qt.ItemIsDropEnabled
[10725]145
[9117]146    def insertRows(self, row, count, parent=QModelIndex()):
147        """ Insert ``count`` rows at ``row``, the list fill be filled
148        with ``None``
149        """
150        if not parent.isValid():
151            self.__setslice__(row, row, [None] * count)
152            return True
153        else:
154            return False
[10725]155
[8042]156    def removeRows(self, row, count, parent=QModelIndex()):
[10725]157        """Remove ``count`` rows starting at ``row``
[9117]158        """
[8042]159        if not parent.isValid():
160            self.__delslice__(row, row + count)
161            return True
162        else:
163            return False
[10725]164
[8042]165    def extend(self, iterable):
166        list_ = list(iterable)
167        self.beginInsertRows(QModelIndex(), len(self), len(self) + len(list_) - 1)
168        self._list.extend(list_)
[9117]169        self._other_data.extend([_store() for _ in list_])
[8042]170        self.endInsertRows()
[10725]171
[8042]172    def append(self, item):
173        self.extend([item])
[10725]174
[8042]175    def insert(self, i, val):
176        self.beginInsertRows(QModelIndex(), i, i)
177        self._list.insert(i, val)
[9117]178        self._other_data.insert(i, _store())
[8042]179        self.endInsertRows()
[10725]180
[8042]181    def remove(self, val):
182        i = self._list.index(val)
183        self.__delitem__(i)
[10725]184
[8042]185    def pop(self, i):
186        item = self._list[i]
187        self.__delitem__(i)
188        return item
[10725]189
[8042]190    def __len__(self):
191        return len(self._list)
[10725]192
[8042]193    def __iter__(self):
194        return iter(self._list)
[10725]195
[8042]196    def __getitem__(self, i):
197        return self._list[i]
[10725]198
[9180]199    def __getslice__(self, i, j):
200        return self._list[i:j]
[10725]201
[8042]202    def __add__(self, iterable):
[10725]203        new_list = PyListModel(list(self._list), 
204                           self.parent(),
205                           flags=self._flags,
206                           list_item_role=self.list_item_role,
207                           supportedDropActions=self.supportedDropActions()
208                           )
209        new_list._other_data = list(self._other_data)
210        new_list.extend(iterable)
211        return new_list
212
[8042]213    def __iadd__(self, iterable):
214        self.extend(iterable)
[10725]215
[8042]216    def __delitem__(self, i):
217        self.beginRemoveRows(QModelIndex(), i, i)
218        del self._list[i]
[9117]219        del self._other_data[i]
[8042]220        self.endRemoveRows()
[10725]221
[8042]222    def __delslice__(self, i, j):
[10947]223        max_i = len(self)
224        i = max(0, min(i, max_i))
225        j = max(0, min(j, max_i))
226
227        if j > i and i < max_i:
[8042]228            self.beginRemoveRows(QModelIndex(), i, j - 1)
229            del self._list[i:j]
[9117]230            del self._other_data[i:j]
[8042]231            self.endRemoveRows()
[10725]232
[8042]233    def __setitem__(self, i, value):
234        self._list[i] = value
235        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(i), self.index(i))
[10725]236
[8042]237    def __setslice__(self, i, j, iterable):
238        self.__delslice__(i, j)
239        all = list(iterable)
240        if len(all):
241            self.beginInsertRows(QModelIndex(), i, i + len(all) - 1)
242            self._list[i:i] = all
[9117]243            self._other_data[i:i] = [_store() for _ in all]
[8042]244            self.endInsertRows()
[10725]245
[8042]246    def reverse(self):
247        self._list.reverse()
[9117]248        self._other_data.reverse()
[8042]249        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(0), self.index(len(self) -1))
[10725]250
[8042]251    def sort(self, *args, **kwargs):
[9117]252        indices = _argsort(self._list, *args, **kwargs)
253        list = [self._list[i] for i in indices]
254        other = [self._other_data[i] for i in indices]
255        for i, new_l, new_o in enumerate(zip(list, other)):
256            self._list[i] = new_l
257            self._other_data[i] = new_o
[8042]258        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(0), self.index(len(self) -1))
[10725]259
[8042]260    def __repr__(self):
261        return "PyListModel(%s)" % repr(self._list)
[10725]262
[8042]263    def __nonzero__(self):
264        return len(self) != 0
[10725]265
[8042]266    #for Python 3000
267    def __bool__(self):
268        return len(self) != 0
[10725]269
[8042]270    def emitDataChanged(self, indexList):
271        if isinstance(indexList, int):
272            indexList = [indexList]
[10725]273
274        #TODO: group indexes into ranges
[8042]275        for ind in indexList:
276            self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(ind), self.index(ind))
[10725]277
[9117]278    ###########
279    # Drag/drop
280    ###########
[10725]281
[9117]282    def supportedDropActions(self):
283        return self._supportedDropActions
[10725]284
[9180]285    def mimeTypes(self):
286        return self.MIME_TYPES + list(QAbstractListModel.mimeTypes(self))
[10725]287
[9180]288    def mimeData(self, indexlist):
289        if len(indexlist) <= 0:
290            return None
[10725]291
[9180]292        items = [self[i.row()] for i in indexlist]
293        mime = QAbstractListModel.mimeData(self, indexlist)
294        data = cPickle.dumps(vars)
295        mime.setData(self.MIME_TYPE, QByteArray(data))
296        mime._items = items
297        return mime
[10725]298
[9180]299    def dropMimeData(self, mime, action, row, column, parent):
300        if action == Qt.IgnoreAction:
301            return True
[10725]302
[9180]303        if not mime.hasFormat(self.MIME_TYPE):
304            return False
[10725]305
[9180]306        if hasattr(mime, "_vars"):
307            vars = mime._vars
308        else:
309            desc = str(mime.data(self.MIME_TYPE))
310            vars = cPickle.loads(desc)
[10725]311
[9180]312        return QAbstractListModel.dropMimeData(self, mime, action, row, column, parent)
[10725]313
314
[11737]315def feature_tooltip(feature):
316    if isinstance(feature, Orange.feature.Discrete):
317        return discrete_feature_tooltip(feature)
318    elif isinstance(feature, Orange.feature.Continuous):
319        return continuous_feature_toltip(feature)
320    elif isinstance(feature, Orange.feature.String):
321        return string_feature_tooltip(feature)
322    elif isinstance(feature, Orange.feature.Python):
323        return python_feature_tooltip(feature)
324    elif isinstance(feature, Orange.feature.Descriptor):
325        return generic_feature_tooltip(feature)
326    else:
327        raise TypeError("Expected an instance of 'Orange.feature.Descriptor'")
328
329
330def feature_labels_tooltip(feature):
331    text = ""
332    if feature.attributes:
333        items = feature.attributes.items()
334        items = [(escape(key), escape(value)) for key, value in items]
335        labels = map("%s = %s".__mod__, items)
336        text += "<br/>Feature Labels:<br/>"
337        text += "<br/>".join(labels)
338    return text
339
340
341def discrete_feature_tooltip(feature):
342    text = ("<b>%s</b><br/>Discrete with %i values: " %
343            (escape(feature.name), len(feature.values)))
344    text += ", ".join("%r" % escape(v) for v in feature.values)
345    text += feature_labels_tooltip(feature)
346    return text
347
348
349def continuous_feature_toltip(feature):
350    text = "<b>%s</b><br/>Continuous" % escape(feature.name)
351    text += feature_labels_tooltip(feature)
352    return text
353
354
355def string_feature_tooltip(feature):
356    text = "<b>%s</b><br/>String" % escape(feature.name)
357    text += feature_labels_tooltip(feature)
358    return text
359
360
361def python_feature_tooltip(feature):
362    text = "<b>%s</b><br/>Python" % escape(feature.name)
363    text += feature_labels_tooltip(feature)
364    return text
365
366
367def generic_feature_tooltip(feature):
368    text = "<b>%s</b><br/>%s" % (escape(feature.name), type(feature).__name__)
369    text += feature_labels_tooltip(feature)
370    return text
371
372
[8042]373class VariableListModel(PyListModel):
[10725]374
[9180]375    MIME_TYPE = "application/x-Orange-VariableList"
[10725]376
[8042]377    def data(self, index, role=Qt.DisplayRole):
[10725]378        if self._is_index_valid_for(index, self):
379            i = index.row()
380            var = self[i]
381            if role == Qt.DisplayRole:
382                return QVariant(var.name)
383            elif role == Qt.DecorationRole:
384                return QVariant(OWGUI.getAttributeIcons().get(var.varType, -1))
385            elif role == Qt.ToolTipRole:
386                return QVariant(self.variable_tooltip(var))
387            else:
388                return PyListModel.data(self, index, role)
[9117]389        else:
[10725]390            return QVariant()
391
[9180]392    def variable_tooltip(self, var):
[10046]393        if isinstance(var, Orange.feature.Discrete):
[9180]394            return self.discrete_variable_tooltip(var)
[10046]395        elif isinstance(var, Orange.feature.Continuous):
[9180]396            return self.continuous_variable_toltip(var)
[10046]397        elif isinstance(var, Orange.feature.String):
[9180]398            return self.string_variable_tooltip(var)
[10725]399
[9180]400    def variable_labels_tooltip(self, var):
[11737]401        return feature_labels_tooltip(var)
[10725]402
[9180]403    def discrete_variable_tooltip(self, var):
[11737]404        return discrete_feature_tooltip(var)
[10725]405
[9180]406    def continuous_variable_toltip(self, var):
[11737]407        return continuous_feature_toltip(var)
[10725]408
[9180]409    def string_variable_tooltip(self, var):
[11737]410        return string_feature_tooltip(var)
[10725]411
[9180]412    def python_variable_tooltip(self, var):
[11737]413        return python_feature_tooltip(var)
414
[10725]415
[9184]416
[11738]417# Back-compatibility
418safe_text = escape
[9184]419
[8042]420
421class ListSingleSelectionModel(QItemSelectionModel):
422    """ Item selection model for list item models with single selection.
423   
424    Defines signal:
425        - selectedIndexChanged(QModelIndex)
426       
427    """
428    def __init__(self, model, parent=None):
429        QItemSelectionModel.__init__(self, model, parent)
430        self.connect(self, SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self.onSelectionChanged)
431       
432    def onSelectionChanged(self, new, old):
433        index = list(new.indexes())
434        if index:
435            index = index.pop()
436        else:
437            index = QModelIndex()
438        self.emit(SIGNAL("selectedIndexChanged(QModelIndex)"), index)
439       
440    def selectedRow(self):
441        """ Return QModelIndex of the selected row or invalid if no selection.
442        """
443        rows = self.selectedRows()
444        if rows:
445            return rows[0]
446        else:
447            return QModelIndex()
448       
449    def select(self, index, flags=QItemSelectionModel.ClearAndSelect):
450        if isinstance(index, int):
451            index = self.model().index(index)
452        return QItemSelectionModel.select(self, index, flags)
453   
454       
455class ModelActionsWidget(QWidget):
456    def __init__(self, actions=[], parent=None, direction=QBoxLayout.LeftToRight):
457        QWidget.__init__(self, parent)
458        self.actions = []
459        self.buttons = []
460        layout = QBoxLayout(direction)
461        layout.setContentsMargins(0, 0, 0, 0)
462        self.setContentsMargins(0, 0, 0, 0)
463        self.setLayout(layout)
464        for action in actions:
465            self.addAction(action)
466        self.setLayout(layout)
467           
468    def actionButton(self, action):
469        if isinstance(action, QAction):
470            button = QToolButton(self)
471            button.setDefaultAction(action)
472            return button
473        elif isinstance(action, QAbstractButton):
474            return action
475           
476    def insertAction(self, ind, action, *args):
477        button = self.actionButton(action)
478        self.layout().insertWidget(ind, button, *args)
479        self.buttons.insert(ind, button)
480        self.actions.insert(ind, action)
481        return button
482       
483    def addAction(self, action, *args):
484        return self.insertAction(-1, action, *args)
485
[10046]486   
Note: See TracBrowser for help on using the repository browser.