#!/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)