Source code for qtypy.model

#!/usr/bin/env python3
#!/usr/bin/env python3

"""
Classes for Qt's model/view programming, with a Pythonic touch.
"""

import logging
import collections
import functools

from PyQt5 import QtCore


[docs]class PythonListModel(QtCore.QAbstractListModel): """ Python list-like object that implements QAbstractListModel. Most list operations like iterating, slicing, etc are supported except for * reverse() * sort() * Slices with step Items may define an `itemDataChanged()` signal, that is to be emitted whenever the item's data changes. """ def __init__(self, parent=None, value=None): super().__init__(parent) self._data = list(value or []) for item in self._data: self._bindItem(item) # Virtual methods for AbstractListModel def rowCount(self, parent): # pylint: disable=W0613 return len(self._data) def data(self, index, role): if role == QtCore.Qt.UserRole: return self._data[index.row()] return None # pragma: no cover def _bindItem(self, item): if hasattr(item, 'itemDataChanged'): item.itemDataChanged.connect(functools.partial(self.__itemChanged, item)) def __itemChanged(self, item): index = self._data.index(item) self.dataChanged.emit(self.createIndex(index, 0), self.createIndex(index, 10), []) # We don't know how many columns there are... # Python mutable sequence / list protocol def __iter__(self): return self._data.__iter__() def __getitem__(self, key): if isinstance(key, int): return self._data[key] if isinstance(key, slice): return self.__class__(value=self._data[key]) raise TypeError('Unsupported key type %s' % type(key)) # pragma: no cover def __len__(self): return len(self._data) def __add__(self, seq): return self.__class__(value=self._data + seq) def __contains__(self, value): return value in self._data def index(self, item, *args): # Hack: QAbstractListModel also has an index method... if args: column, parent = args if parent.isValid(): return QtCore.QModelIndex() return self.createIndex(item, column) return self._data.index(item)
[docs] def copy(self): """ Returns a shallow copy of this object (same class, same content) """ return self.__class__(value=self._data[:])
def count(self, value): return self._data.count(value) def __setitem__(self, key, value): if isinstance(key, int): self.beginRemoveRows(QtCore.QModelIndex(), key, key) del self._data[key] self.endRemoveRows() self.beginInsertRows(QtCore.QModelIndex(), key, key) self._data.insert(key, value) self._bindItem(value) self.endInsertRows() elif isinstance(key, slice): start, stop, step = key.indices(len(self._data)) assert step == 1, 'Step is not supported' if stop >= start: if stop > start: self.beginRemoveRows(QtCore.QModelIndex(), start, stop - 1) del self._data[start:stop] self.endRemoveRows() self.beginInsertRows(QtCore.QModelIndex(), start, start + len(value) - 1) for item in value: self._bindItem(item) self._data[start:start] = value self.endInsertRows() else: # pragma: no cover raise TypeError('Unsupported key type %s' % type(key)) def __delitem__(self, key): if isinstance(key, int): self.beginRemoveRows(QtCore.QModelIndex(), key, key) del self._data[key] self.endRemoveRows() elif isinstance(key, slice): start, stop, step = key.indices(len(self._data)) assert step == 1, 'Step is not supported' if stop > start: self.beginRemoveRows(QtCore.QModelIndex(), start, stop - 1) del self._data[key] self.endRemoveRows() else: # pragma: no cover raise TypeError('Unsupported key type %s' % type(key)) def __iadd__(self, seq): self.beginInsertRows(QtCore.QModelIndex(), len(self._data), len(self._data) + len(seq) - 1) for item in seq: self._bindItem(item) self._data += seq self.endInsertRows() return self @staticmethod def reverse(): # pragma: no cover raise RuntimeError('Not supported') @staticmethod def sort(): # pragma: no cover raise RuntimeError('Not supported') def remove(self, item): idx = self.index(item) self.pop(idx) return idx def pop(self, index=None): if index is None: index = len(self._data) - 1 self.beginRemoveRows(QtCore.QModelIndex(), index, index) item = self._data.pop(index) self.endRemoveRows() return item def clear(self): self.beginRemoveRows(QtCore.QModelIndex(), 0, len(self._data) - 1) self._data = [] self.endRemoveRows() def append(self, item): count = len(self._data) self.beginInsertRows(QtCore.QModelIndex(), count, count) self._bindItem(item) self._data.append(item) self.endInsertRows() def insert(self, index, item): self.beginInsertRows(QtCore.QModelIndex(), index, index) self._bindItem(item) self._data.insert(index, item) self.endInsertRows() def extend(self, items): count = len(self._data) self.beginInsertRows(QtCore.QModelIndex(), count, count + len(items) - 1) for item in items: self._bindItem(item) self._data.extend(items) self.endInsertRows()
[docs]class PythonTreeModel(QtCore.QAbstractItemModel): """ This is an implementation of QAbstractItemModel based on a :class:`PythonListModel`. Elements of the list model that are themselves instances of :class:`PythonListModel` will have children. """ def __init__(self, model=None, parent=None): super().__init__(parent) self._model = PythonListModel() if model is None else model self.__bindItem(None, self._model) def root(self): return self._model def __bindItem(self, container, item): if hasattr(item, 'itemDataChanged') and container is not None: item.itemDataChanged.connect(functools.partial(self.__itemDataChanged, container, item)) if isinstance(item, PythonListModel): item.rowsAboutToBeInserted.connect(lambda pi, f, l: self.__itemChildrenAdded_Before(item, f, l)) item.rowsInserted.connect(lambda pi, f, l: self.__itemChildrenAdded_After(item, f, l)) item.rowsAboutToBeRemoved.connect(lambda pi, f, l: self.__itemChildrenRemoved_Before(item, f, l)) item.rowsRemoved.connect(lambda pi, f, l: self.__itemChildrenRemoved_After()) for child in item: self.__bindItem(item, child) def __itemDataChanged(self, container, item): tl = self.createIndex(container.index(item), 0, container) br = self.createIndex(container.index(item), 10, container) # XXXFIXME self.dataChanged.emit(tl, br) def __itemChildrenAdded_Before(self, container, first, last): self.beginInsertRows(self._indexOf(container), first, last) def __itemChildrenAdded_After(self, container, first, last): self.endInsertRows() for index in range(first, last + 1): self.__bindItem(container, container[index]) def __itemChildrenRemoved_Before(self, container, first, last): self.beginRemoveRows(self._indexOf(container), first, last) def __itemChildrenRemoved_After(self): self.endRemoveRows() def _find_parent(self, container, item): # Mmmmh. This is quite inefficient. if item in container: return container for child in container: if isinstance(child, PythonListModel): parent = self._find_parent(child, item) if parent is not None: return parent return None def _indexOf(self, item, column=0): if item is self._model: return QtCore.QModelIndex() parent = self._find_parent(self._model, item) return self.createIndex(parent.index(item), column, parent)
[docs] def itemContainer(self, item): """ Looks up the item's container """ return self._find_parent(self._model, item)
[docs] def itemParent(self, item): """ Looks up the item's parent in the tree """ container = self.itemContainer(item) return None if container is self._model else container
# AbstractItemModel methods def index(self, row, col, parentIndex): if parentIndex.isValid(): parent = parentIndex.internalPointer()[parentIndex.row()] return self.createIndex(row, col, parent) return self.createIndex(row, col, self._model) def parent(self, index): parent = index.internalPointer() if parent is self._model: return QtCore.QModelIndex() gparent = self._find_parent(self._model, parent) return self.createIndex(gparent.index(parent), 0, gparent) def columnCount(self, parentIndex): # pylint: disable=W0613,R0201 return 1 # Must be implemented and not 0, even when using a proxy model def rowCount(self, parentIndex): if parentIndex.isValid(): parent = parentIndex.internalPointer()[parentIndex.row()] try: return len(parent) except TypeError: # Not a container return 0 return len(self._model) def data(self, index, role): # pylint: disable=R0201 if role == QtCore.Qt.UserRole: return index.internalPointer()[index.row()] return None
class _ColumnedTreeModel(QtCore.QIdentityProxyModel): """ This is a proxy model used internally to decouple the column stuff from the underlying model. """ logger = logging.getLogger('qtypy._ColumnedTreeModel') resizeModeChanged = QtCore.pyqtSignal(int, int) visibilityChanged = QtCore.pyqtSignal(int, bool) ColType = collections.namedtuple('ColType', ['column', 'visible']) def __init__(self, sourceModel, parent=None): super().__init__(parent) self._columns = [] self.setSourceModel(sourceModel) def addColumn(self, column, visible=True): column.model = self index = len(self._columns) self.beginInsertColumns(QtCore.QModelIndex(), index, index) self._columns.append(self.ColType(column, visible)) self.endInsertColumns() column.nameChanged.connect(functools.partial(self._columnNameChanged, column)) return index def setColumnVisible(self, column, visible): index = self._columnIndex(column) if index is not None: self._columns[index] = self._columns[index]._replace(visible=visible) self.visibilityChanged.emit(index, visible) def setColumnResizeMode(self, column, mode): index = self._columnIndex(column) if index is not None: self.resizeModeChanged.emit(index, mode) def visibleColumns(self): for col in self._columns: if col.visible: yield col.column def columns(self): for col in self._columns: yield col.column def isColumnVisible(self, column): index = self._columnIndex(column) if index is not None: return self._columns[index].visible return False def columnCount(self, parentIndex): # pylint: disable=W0613 # Returning the actual column count for this parent fucks things up; return the max # and handle IndexError in flags/data. return len(self._columns) def flags(self, index): flags = super().flags(index) if index.isValid(): item = index.internalPointer()[index.row()] column = self._columns[index.column()].column if column.appliesToChildrenOf(self.sourceModel().itemParent(item)): flags |= column.flags() return flags def data(self, index, role): assert index.isValid() item = index.internalPointer()[index.row()] column = self._columns[index.column()].column if not column.appliesToChildrenOf(self.sourceModel().itemParent(item)): return super().data(index, role) data = column.data(item, role) if data is None: data = super().data(index, role) return data def setData(self, index, value, role): item = index.internalPointer()[index.row()] column = self._columns[index.column()].column if not column.appliesToChildrenOf(self.sourceModel().itemParent(item)): return super().setData(index, value, role) return column.setData(item, role, value) def headerData(self, section, orientation, role): if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: column = self._columns[section].column return column.name() return super().headerData(section, orientation, role) def _columnIndex(self, column): for index, col in enumerate(self._columns): if col.column == column: return index return None def _columnNameChanged(self, column): index = self._columnIndex(column) if index is not None: self.headerDataChanged.emit(QtCore.Qt.Horizontal, index, index)