from PySide2 import QtCore class QObjectListModel(QtCore.QAbstractListModel): """ QObjectListModel provides a more powerful, but still easy to use, alternative to using QObjectList lists as models for QML views. As a QAbstractListModel, it has the ability to automatically notify the view of specific changes to the list, such as adding or removing items. At the same time it provides QList-like convenience functions such as append, at, and removeAt for easily working with the model from Python. """ def __init__(self, keyAttrName='', parent=None): """ Constructs an object list model with the given parent. """ super(QObjectListModel, self).__init__(parent) self._objects = list() # Internal list of objects self._keyAttrName = keyAttrName self._objectByKey = {} self.roles = QtCore.QAbstractListModel.roleNames(self) self.ObjectRole = QtCore.Qt.UserRole self.roles[self.ObjectRole] = b"object" def roleNames(self): return self.roles def __iter__(self): """ Enables iteration over the list of objects. """ return iter(self._objects) def keys(self): return self._objectByKey.keys() def __len__(self): return self.size() def __bool__(self): return self.size() > 0 def __getitem__(self, index): """ Enables the [] operator """ return self._objects[index] def data(self, index, role): """ Returns data for the specified role, from the item with the given index. The only valid role is ObjectRole. If the view requests an invalid index or role, an invalid variant is returned. """ if index.row() < 0 or index.row() >= len(self._objects): return None return self._objects[index.row()] def rowCount(self, parent): """ Returns the number of rows in the model. This value corresponds to the number of items in the model's internal object list. """ return self.size() def objectList(self): """ Returns the object list used by the model to store data. """ return self._objects def setObjectList(self, objects): """ Sets the model's internal objects list to objects. The model will notify any attached views that its underlying data has changed. """ oldSize = self.size() self.beginResetModel() for obj in self._objects: self._dereferenceItem(obj) self._objects = objects for obj in self._objects: self._referenceItem(obj) self.endResetModel() self.dataChanged.emit(self.index(0), self.index(self.size() - 1), []) if self.size() != oldSize: self.countChanged.emit() # ###### # BaseModel API # ###### @property def objects(self): return self._objectByKey @QtCore.Slot(str, result=QtCore.QObject) def get(self, key): """ Raises a KeyError if key is not in the map. :param key: :return: """ return self._objectByKey[key] def add(self, obj): self.append(obj) def pop(self, key): obj = self.get(key) self.remove(obj) return obj ############ # List API # ############ def append(self, obj): """ Insert object at the end of the model. """ self.extend([obj]) def extend(self, iterable): """ Insert objects at the end of the model. """ self.beginInsertRows(QtCore.QModelIndex(), self.size(), self.size() + len(iterable) - 1) [self._referenceItem(obj) for obj in iterable] self._objects.extend(iterable) self.endInsertRows() self.countChanged.emit() def insert(self, i, toInsert): """ Inserts object(s) at index position i in the model and notifies any views. If i is 0, the object is prepended to the model. If i is size(), the object is appended to the list. Accepts both QObject and list of QObjects. """ if not isinstance(toInsert, list): toInsert = [toInsert] self.beginInsertRows(QtCore.QModelIndex(), i, i + len(toInsert) - 1) for obj in reversed(toInsert): self._referenceItem(obj) self._objects.insert(i, obj) self.endInsertRows() self.countChanged.emit() @QtCore.Slot(int, result=QtCore.QObject) def at(self, i): """ Return the object at index i. """ return self._objects[i] def replace(self, i, obj): """ Replaces the item at index position i with object and notifies any views. i must be a valid index position in the list (i.e., 0 <= i < size()). """ self._dereferenceItem(self._objects[i]) self._referenceItem(obj) self._objects[i] = obj self.dataChanged.emit(self.index(i), self.index(i), []) def move(self, fromIndex, toIndex): """ Moves the item at index position from to index position to and notifies any views. This function assumes that both from and to are at least 0 but less than size(). To avoid failure, test that both from and to are at least 0 and less than size(). """ value = toIndex if toIndex > fromIndex: value += 1 if not self.beginMoveRows(QtCore.QModelIndex(), fromIndex, fromIndex, QtCore.QModelIndex(), value): return self._objects.insert(toIndex, self._objects.pop(fromIndex)) self.endMoveRows() def removeAt(self, i, count=1): """ Removes count number of items from index position i and notifies any views. i must be a valid index position in the model (i.e., 0 <= i < size()), as must as i + count - 1. """ self.beginRemoveRows(QtCore.QModelIndex(), i, i + count - 1) for cpt in range(count): obj = self._objects.pop(i) self._dereferenceItem(obj) self.endRemoveRows() self.countChanged.emit() def remove(self, obj): """ Removes the first occurrence of the given object. Raises a ValueError if not in list. """ if not self.contains(obj): raise ValueError("QObjectListModel.remove(obj) : obj not in list") self.removeAt(self.indexOf(obj)) def takeAt(self, i): """ Removes the item at index position i (notifying any views) and returns it. i must be a valid index position in the model (i.e., 0 <= i < size()). """ self.beginRemoveRows(QtCore.QModelIndex(), i, i) obj = self._objects.pop(i) self._dereferenceItem(obj) self.endRemoveRows() self.countChanged.emit() return obj def clear(self): """ Removes all items from the model and notifies any views. """ if not self._objects: return self.beginResetModel() for obj in self._objects: self._dereferenceItem(obj) self._objects = [] self.endResetModel() self.countChanged.emit() def update(self, objects): self.extend(objects) def reset(self, objects): self.clear() self.update(objects) def contains(self, obj): """ Returns true if the list contains an occurrence of object; otherwise returns false. """ return obj in self._objects def indexOf(self, matchObj, fromIndex=0, positive=True): """ Returns the index position of the first occurrence of object in the model, searching forward from index position from. If positive is True, will always return a positive index. """ index = self._objects[fromIndex:].index(matchObj) + fromIndex if positive and index < 0: index += self.size() return index def lastIndexOf(self, matchObj, fromIndex=-1, positive=True): """ Returns the index position of the last occurrence of object in the list, searching backward from index position from. If from is -1 (the default), the search starts at the last item. If positive is True, will always return a positive index. """ r = list(self._objects) r.reverse() index = - r[-fromIndex - 1:].index(matchObj) + fromIndex if positive and index < 0: index += self.size() return index def size(self): """ Returns the number of items in the model. """ return len(self._objects) @QtCore.Slot(result=bool) def isEmpty(self): """ Returns true if the model contains no items; otherwise returns false. """ return len(self._objects) == 0 def _referenceItem(self, item): if not self._keyAttrName: return key = getattr(item, self._keyAttrName, None) if key is None: return if key in self._objectByKey: raise ValueError("Object key {}:{} is not unique".format(self._keyAttrName, key)) self._objectByKey[key] = item def _dereferenceItem(self, item): if not self._keyAttrName: return key = getattr(item, self._keyAttrName, None) if key is None: return assert key in self._objectByKey self._objectByKey.pop(key) countChanged = QtCore.Signal() count = QtCore.Property(int, size, notify=countChanged) class QTypedObjectListModel(QObjectListModel): """ Typed QObjectListModel that exposes T properties as roles """ # TODO: handle notify signal to emit dataChanged signal def __init__(self, keyAttrName="name", T=QtCore.QObject, parent=None): super(QTypedObjectListModel, self).__init__(keyAttrName, parent) self._T = T blacklist = ["id", "index", "class", "model", "modelData"] self._metaObject = T.staticMetaObject propCount = self._metaObject.propertyCount() role = self.ObjectRole + 1 for i in range(0, propCount): prop = self._metaObject.property(i) if not prop.name() in blacklist: self.roles[role] = prop.name() role += 1 else: print("Reserved role name: " + prop.name()) def data(self, index, role): obj = super(QTypedObjectListModel, self).data(index, self.ObjectRole) if role == self.ObjectRole: return obj if obj: return obj.property(self.roles[role]) return None def roleForName(self, name): roles = [role for role, value in self.roles.items() if value == name] return roles[0] if roles else -1 def _referenceItem(self, item): if item.staticMetaObject != self._metaObject: raise TypeError("Invalid object type: expected {}, got {}".format( self._metaObject.className(), item.staticMetaObject.className())) super(QTypedObjectListModel, self)._referenceItem(item) Model = QObjectListModel DictModel = QObjectListModel Slot = QtCore.Slot Signal = QtCore.Signal Property = QtCore.Property BaseObject = QtCore.QObject Variant = "QVariant"