source: orange/Orange/OrangeWidgets/OWItemModels.py @ 11737:6377d02bd026

Revision 11737:6377d02bd026, 18.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Refactor feature tool tip functions.

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