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

Line 
1
2import cPickle
3
4from xml.sax.saxutils import escape
5from functools import wraps, partial
6from collections import defaultdict
7from contextlib import contextmanager
8
9from PyQt4.QtCore import *
10from PyQt4.QtGui import *
11
12import OWGUI
13import Orange
14
15
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
30
31@contextmanager
32def signal_blocking(object):
33    blocked = object.signalsBlocked()
34    object.blockSignals(True)
35    yield
36    object.blockSignals(blocked)
37
38
39class PyListModel(QAbstractListModel):
40    """ A model for displaying python list like objects in Qt item view classes
41    """
42    MIME_TYPES = ["application/x-Orange-PyListModelData"]
43
44    def __init__(self, iterable=[], parent=None,
45                 flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,
46                 list_item_role=Qt.DisplayRole,
47                 supportedDropActions=Qt.MoveAction):
48        QAbstractListModel.__init__(self, parent)
49        self._list = []
50        self._other_data = []
51        self._flags = flags
52        self.list_item_role = list_item_role
53
54        self._supportedDropActions = supportedDropActions
55        self.extend(iterable)
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
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
71        self._other_data = [_store() for _ in list]
72        self.reset()
73
74    def index(self, row, column=0, parent=QModelIndex()):
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
80    def headerData(self, section, orientation, role=Qt.DisplayRole):
81        if role == Qt.DisplayRole:
82            return QVariant(str(section))
83
84    def rowCount(self, parent=QModelIndex()):
85        return 0 if parent.isValid() else len(self)
86
87    def columnCount(self, parent=QModelIndex()):
88        return 0 if parent.isValid() else 1
89
90    def data(self, index, role=Qt.DisplayRole):
91        row = index.row()
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()))
97        else:
98            return QVariant()
99
100    def itemData(self, index):
101        map = QAbstractListModel.itemData(self, index)
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:
108            map[key] = QVariant(value)
109
110        return map
111
112    def parent(self, index=QModelIndex()):
113        return QModelIndex()
114
115    def setData(self, index, value, role=Qt.EditRole):
116        if role == Qt.EditRole and self._is_index_valid_for(index, self):
117            obj = value.toPyObject()
118            self[index.row()] = obj # Will emit proper dataChanged signal
119            return True
120        elif self._is_index_valid_for(index, self._other_data):
121            self._other_data[index.row()][role] = value
122            self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
123            return True
124        else:
125            return False
126
127    def setItemData(self, index, data):
128        data = dict(data)
129        with signal_blocking(self):
130            for role, value in data.items():
131                if role == Qt.EditRole and \
132                        self._is_index_valid_for(index, self):
133                    self[index.row()] = value.toPyObject()
134                elif self._is_index_valid_for(index, self._other_data):
135                    self._other_data[index.row()][role] = value
136
137        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
138        return True
139
140    def flags(self, index):
141        if self._is_index_valid_for(index, self._other_data):
142            return self._other_data[index.row()].get("flags", self._flags)
143        else:
144            return self._flags | Qt.ItemIsDropEnabled
145
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
155
156    def removeRows(self, row, count, parent=QModelIndex()):
157        """Remove ``count`` rows starting at ``row``
158        """
159        if not parent.isValid():
160            self.__delslice__(row, row + count)
161            return True
162        else:
163            return False
164
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_)
169        self._other_data.extend([_store() for _ in list_])
170        self.endInsertRows()
171
172    def append(self, item):
173        self.extend([item])
174
175    def insert(self, i, val):
176        self.beginInsertRows(QModelIndex(), i, i)
177        self._list.insert(i, val)
178        self._other_data.insert(i, _store())
179        self.endInsertRows()
180
181    def remove(self, val):
182        i = self._list.index(val)
183        self.__delitem__(i)
184
185    def pop(self, i):
186        item = self._list[i]
187        self.__delitem__(i)
188        return item
189
190    def __len__(self):
191        return len(self._list)
192
193    def __iter__(self):
194        return iter(self._list)
195
196    def __getitem__(self, i):
197        return self._list[i]
198
199    def __getslice__(self, i, j):
200        return self._list[i:j]
201
202    def __add__(self, iterable):
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
213    def __iadd__(self, iterable):
214        self.extend(iterable)
215
216    def __delitem__(self, i):
217        self.beginRemoveRows(QModelIndex(), i, i)
218        del self._list[i]
219        del self._other_data[i]
220        self.endRemoveRows()
221
222    def __delslice__(self, i, j):
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:
228            self.beginRemoveRows(QModelIndex(), i, j - 1)
229            del self._list[i:j]
230            del self._other_data[i:j]
231            self.endRemoveRows()
232
233    def __setitem__(self, i, value):
234        self._list[i] = value
235        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(i), self.index(i))
236
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
243            self._other_data[i:i] = [_store() for _ in all]
244            self.endInsertRows()
245
246    def reverse(self):
247        self._list.reverse()
248        self._other_data.reverse()
249        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(0), self.index(len(self) -1))
250
251    def sort(self, *args, **kwargs):
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
258        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(0), self.index(len(self) -1))
259
260    def __repr__(self):
261        return "PyListModel(%s)" % repr(self._list)
262
263    def __nonzero__(self):
264        return len(self) != 0
265
266    #for Python 3000
267    def __bool__(self):
268        return len(self) != 0
269
270    def emitDataChanged(self, indexList):
271        if isinstance(indexList, int):
272            indexList = [indexList]
273
274        #TODO: group indexes into ranges
275        for ind in indexList:
276            self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(ind), self.index(ind))
277
278    ###########
279    # Drag/drop
280    ###########
281
282    def supportedDropActions(self):
283        return self._supportedDropActions
284
285    def mimeTypes(self):
286        return self.MIME_TYPES + list(QAbstractListModel.mimeTypes(self))
287
288    def mimeData(self, indexlist):
289        if len(indexlist) <= 0:
290            return None
291
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
298
299    def dropMimeData(self, mime, action, row, column, parent):
300        if action == Qt.IgnoreAction:
301            return True
302
303        if not mime.hasFormat(self.MIME_TYPE):
304            return False
305
306        if hasattr(mime, "_vars"):
307            vars = mime._vars
308        else:
309            desc = str(mime.data(self.MIME_TYPE))
310            vars = cPickle.loads(desc)
311
312        return QAbstractListModel.dropMimeData(self, mime, action, row, column, parent)
313
314
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
373class VariableListModel(PyListModel):
374
375    MIME_TYPE = "application/x-Orange-VariableList"
376
377    def data(self, index, role=Qt.DisplayRole):
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)
389        else:
390            return QVariant()
391
392    def variable_tooltip(self, var):
393        if isinstance(var, Orange.feature.Discrete):
394            return self.discrete_variable_tooltip(var)
395        elif isinstance(var, Orange.feature.Continuous):
396            return self.continuous_variable_toltip(var)
397        elif isinstance(var, Orange.feature.String):
398            return self.string_variable_tooltip(var)
399
400    def variable_labels_tooltip(self, var):
401        return feature_labels_tooltip(var)
402
403    def discrete_variable_tooltip(self, var):
404        return discrete_feature_tooltip(var)
405
406    def continuous_variable_toltip(self, var):
407        return continuous_feature_toltip(var)
408
409    def string_variable_tooltip(self, var):
410        return string_feature_tooltip(var)
411
412    def python_variable_tooltip(self, var):
413        return python_feature_tooltip(var)
414
415
416
417# Back-compatibility
418safe_text = escape
419
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
486   
Note: See TracBrowser for help on using the repository browser.