source: orange/Orange/OrangeWidgets/OWItemModels.py @ 10725:d7ba31d156e7

Revision 10725:d7ba31d156e7, 17.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Added checks for valid indices in Qt's get set methods for PyListModel (return an invalid QVariant's instead of raising exceptions).

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