moment/harmonyqml/backend/model/list_model.py
miruka 424405e3e8 Set invited and left rooms position in model
Invited rooms at the start, left rooms at the end, with the bonus effect
of fixing the roomList height problem.
2019-04-21 16:53:16 -04:00

235 lines
6.7 KiB
Python

import logging
from typing import (
Any, Callable, Dict, Iterable, List, Mapping, MutableSequence, Optional,
Sequence, Tuple, Union
)
from PyQt5.QtCore import (
QAbstractListModel, QModelIndex, QObject, Qt, pyqtProperty, pyqtSignal,
pyqtSlot
)
from .items import ListItem
NewItem = Union[ListItem, Mapping[str, Any], Sequence]
class ListModel(QAbstractListModel):
changed = pyqtSignal()
def __init__(self,
initial_data: Optional[List[NewItem]] = None,
container: Callable[..., MutableSequence] = list,
parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self._data: MutableSequence[ListItem] = container()
if initial_data:
self.extend(initial_data)
def __repr__(self) -> str:
return "%s(%r)" % (type(self).__name__, self._data)
def __getitem__(self, index):
return self._data[index]
def __setitem__(self, index, value) -> None:
self.set(index, value)
def __delitem__(self, index) -> None:
self.remove(index)
def __len__(self) -> int:
return self.rowCount()
@pyqtProperty(list)
def roles(self) -> Tuple[str, ...]:
return self._data[0].roles if self._data else () # type: ignore
def roleNames(self) -> Dict[int, bytes]:
return {Qt.UserRole + i: bytes(f, "utf-8")
for i, f in enumerate(self.roles, 1)} \
if self._data else {}
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if role <= Qt.UserRole:
return None
return getattr(self._data[index.row()],
str(self.roleNames()[role], "utf8"))
def rowCount(self, _: QModelIndex = QModelIndex()) -> int:
return len(self._data)
def _convert_new_value(self, value: NewItem) -> ListItem:
def convert() -> ListItem:
if self._data and isinstance(value, Mapping):
assert set(value.keys()) <= set(self.roles), \
f"{value}: must have all these keys: {self.roles}"
return type(self._data[0])(**value)
if not self._data and isinstance(value, Mapping):
raise NotImplementedError("First item must be set from Python")
if self._data and isinstance(value, type(self._data[0])):
return value
if not self._data and isinstance(value, ListItem):
return value
raise TypeError("%r: must be mapping or %s" % (
value,
type(self._data[0]).__name__ if self._data else "ListItem"
))
value = convert()
value.setParent(self)
return value
@pyqtProperty(int, constant=True)
def count(self) -> int:
return self.rowCount()
@pyqtSlot(int, result="QVariant")
def get(self, index: int) -> ListItem:
return self._data[index]
@pyqtSlot(str, "QVariant", result=int)
def indexWhere(self, prop: str, is_value: Any) -> int:
for i, item in enumerate(self._data):
if getattr(item, prop) == is_value:
return i
raise ValueError(f"No item in model data with "
f"property {prop!r} set to {is_value!r}.")
@pyqtSlot(str, "QVariant", result="QVariant")
def getWhere(self, prop: str, is_value: Any) -> ListItem:
return self.get(self.indexWhere(prop, is_value))
@pyqtSlot(int, "QVariantMap")
def insert(self, index: int, value: NewItem) -> None:
value = self._convert_new_value(value)
self.beginInsertRows(QModelIndex(), index, index)
self._data.insert(index, value)
self.endInsertRows()
self.changed.emit()
@pyqtSlot("QVariantMap")
def append(self, value: NewItem) -> None:
self.insert(self.rowCount(), value)
@pyqtSlot(list)
def extend(self, values: Iterable[NewItem]) -> None:
for val in values:
self.append(val)
@pyqtSlot("QVariantMap")
def update(self, index: int, value: NewItem) -> None:
value = self._convert_new_value(value)
for role in self.roles:
if role in value.no_update:
continue
setattr(self._data[index], role, getattr(value, role))
qidx = QAbstractListModel.index(self, index, 0)
self.dataChanged.emit(qidx, qidx, self.roleNames())
self.changed.emit()
@pyqtSlot(str, "QVariant", "QVariantMap")
def updateOrAppendWhere(
self, prop: str, is_value: Any, update_with: NewItem
) -> None:
try:
index = self.indexWhere(prop, is_value)
self.update(index, update_with)
except ValueError:
index = self.rowCount()
self.append(update_with)
@pyqtSlot(int, list)
def set(self, index: int, value: NewItem) -> None:
qidx = QAbstractListModel.index(self, index, 0)
value = self._convert_new_value(value)
self._data[index] = value
self.dataChanged.emit(qidx, qidx, self.roleNames())
self.changed.emit()
@pyqtSlot(int, str, "QVariant")
def setProperty(self, index: int, prop: str, value: Any) -> None:
setattr(self._data[index], prop, value)
qidx = QAbstractListModel.index(self, index, 0)
self.dataChanged.emit(qidx, qidx, self.roleNames())
self.changed.emit()
@pyqtSlot(int, int)
@pyqtSlot(int, int, int)
def move(self, from_: int, to: int, n: int = 1) -> None:
# pylint: disable=invalid-name
qlast = from_ + n - 1
if (n <= 0) or (from_ == to) or (qlast == to) or \
not (self.rowCount() > qlast >= 0) or \
not self.rowCount() >= to >= 0:
return
qidx = QModelIndex()
qto = min(self.rowCount(), to + n if to > from_ else to)
# print(f"self.beginMoveRows(qidx, {from_}, {qlast}, qidx, {qto})")
valid = self.beginMoveRows(qidx, from_, qlast, qidx, qto)
if not valid:
logging.warning("Invalid move operation")
return
last = from_ + n
cut = self._data[from_:last]
del self._data[from_:last]
self._data[to:to] = cut
self.endMoveRows()
self.changed.emit()
@pyqtSlot(int)
def remove(self, index: int) -> None:
self.beginRemoveRows(QModelIndex(), index, index)
del self._data[index]
self.endRemoveRows()
self.changed.emit()
@pyqtSlot()
def clear(self) -> None:
# Reimplemented for performance reasons (begin/endRemoveRows)
self.beginRemoveRows(QModelIndex(), 0, self.rowCount())
self._data.clear()
self.endRemoveRows()
self.changed.emit()