Meshroom/meshroom/common/qt.py

391 lines
13 KiB
Python

from PySide6 import QtCore, QtQml
import shiboken6
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.
"""
ObjectRole = QtCore.Qt.UserRole
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.roles[self.ObjectRole] = b"object"
self.requestDeletion.connect(self.onRequestDeletion, QtCore.Qt.QueuedConnection)
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 items(self):
return self._objectByKey.items()
def __len__(self):
return self.size()
def __bool__(self):
return self.size() > 0
def __getitem__(self, index):
""" Enables the [] operator.
Only accepts index (integer).
"""
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 values(self):
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):
"""
:param key:
:return: the value or None if not found
"""
return self._objectByKey.get(key)
@QtCore.Slot(str, result=QtCore.QObject)
def getr(self, key):
"""
Get or raise an error if the key does not exists.
:param key:
:return: the value
"""
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 #
############
@QtCore.Slot(QtCore.QObject)
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()
@QtCore.Slot(QtCore.QObject)
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.setObjectList(objects)
@QtCore.Slot(QtCore.QObject, result=bool)
def contains(self, obj):
""" Returns true if the list contains an occurrence of object;
otherwise returns false.
"""
return obj in self._objects
@QtCore.Slot(QtCore.QObject, result=int)
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 item.parent():
# Take ownership of the object if not already parented
item.setParent(self)
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):
# Ask for object deletion if parented to the model
if shiboken6.isValid(item) and item.parent() == self:
# delay deletion until the next event loop
# This avoids warnings when the QML engine tries to evaluate (but should not)
# an object that has already been deleted
self.requestDeletion.emit(item)
if not self._keyAttrName:
return
key = getattr(item, self._keyAttrName, None)
if key is None:
return
assert key in self._objectByKey
del self._objectByKey[key]
def onRequestDeletion(self, item):
item.deleteLater()
countChanged = QtCore.Signal()
count = QtCore.Property(int, size, notify=countChanged)
requestDeletion = QtCore.Signal(QtCore.QObject)
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)
class SortedModelByReference(QtCore.QSortFilterProxyModel):
""" Sort a source model based on the ordered list (reference) of the same elements.
This proxy is useful if the model needs to be sorted a certain way for a specific use.
"""
def __init__(self, parent):
super(SortedModelByReference, self).__init__(parent)
self._reference = []
def setReference(self, iterable):
""" Set the reference ordered list """
self._reference = iterable
self.sort()
def reference(self):
return self._reference
def lessThan(self, left, right):
l = self.sourceModel().data(left, QObjectListModel.ObjectRole)
r = self.sourceModel().data(right, QObjectListModel.ObjectRole)
if l not in self._reference:
return False
if r not in self._reference:
return True
return self._reference.index(l) < self._reference.index(r)
def sort(self):
""" Sort the proxy and call invalidate() """
super(SortedModelByReference, self).sort(0, QtCore.Qt.AscendingOrder)
self.invalidate()
DictModel = QObjectListModel
ListModel = QObjectListModel
Slot = QtCore.Slot
Signal = QtCore.Signal
Property = QtCore.Property
BaseObject = QtCore.QObject
Variant = "QVariant"
VariantList = "QVariantList"
JSValue = QtQml.QJSValue