Capitalization, list model and room header work
- Standardized capitalization for variables and file names everywhere in QML and JS, get rid of mixed camelCase/snakeCase, use camelCase like everywhere in Qt - ListModel items are now stored and returned as real QObjects with PyQt properties and signals. This makes dynamic property binding a lot easier and eliminates the need for many hacks. - New update(), updateOrAppendWhere() methods and roles property for ListModel - RoomHeader now properly updates when the room title or topic changes - Add Backend.pdb(), to make it easier to start the debugger from QML
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
from . import items
|
||||
from .list_model import ListModel
|
||||
from .list_model_map import ListModelMap
|
||||
from .qml_models import QMLModels
|
||||
from . import enums, items
|
||||
|
@@ -1,31 +0,0 @@
|
||||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Activity(Enum):
|
||||
none = 0
|
||||
focus = 1
|
||||
paused_typing = 2
|
||||
typing = 3
|
||||
|
||||
|
||||
class Presence(Enum):
|
||||
none = 0
|
||||
offline = 1
|
||||
invisible = 2
|
||||
away = 3
|
||||
busy = 4
|
||||
online = 5
|
||||
|
||||
|
||||
class MessageKind(Enum):
|
||||
audio = "m.audio"
|
||||
emote = "m.emote"
|
||||
file = "m.file"
|
||||
image = "m.image"
|
||||
location = "m.location"
|
||||
notice = "m.notice"
|
||||
text = "m.text"
|
||||
video = "m.video"
|
@@ -1,29 +1,83 @@
|
||||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
from typing import Any, Callable, Optional, Tuple, Union
|
||||
|
||||
from typing import Dict, List, NamedTuple, Optional
|
||||
|
||||
from PyQt5.QtCore import QDateTime
|
||||
|
||||
from ..pyqt_future import PyQtFuture
|
||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||
|
||||
|
||||
class User(NamedTuple):
|
||||
user_id: str
|
||||
display_name: PyQtFuture
|
||||
avatar_url: Optional[str] = None
|
||||
status_message: Optional[str] = None
|
||||
class ListItem(QObject):
|
||||
roles: Tuple[str, ...] = ()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__()
|
||||
|
||||
for role, value in zip(self.roles, args):
|
||||
setattr(self, role, value)
|
||||
|
||||
for role, value in kwargs.items():
|
||||
setattr(self, role, value)
|
||||
|
||||
|
||||
class Room(NamedTuple):
|
||||
room_id: str
|
||||
display_name: Optional[str]
|
||||
description: str = ""
|
||||
typing_users: List[str] = []
|
||||
def __repr__(self) -> str:
|
||||
return "%s(%s)" % (
|
||||
type(self).__name__,
|
||||
", ".join((f"{r}={getattr(self, r)!r}" for r in self.roles)),
|
||||
)
|
||||
|
||||
|
||||
class RoomEvent(NamedTuple):
|
||||
type: str
|
||||
date_time: QDateTime
|
||||
dict: Dict[str, str]
|
||||
is_local_echo: bool = False
|
||||
@pyqtProperty(str, constant=True)
|
||||
def repr(self) -> str:
|
||||
return repr(self)
|
||||
|
||||
|
||||
def prop(qt_type: Union[str, Callable],
|
||||
name: str,
|
||||
signal: Optional[pyqtSignal] = None,
|
||||
default_value: Any = None) -> pyqtProperty:
|
||||
|
||||
def fget(self, name=name, default_value=default_value):
|
||||
if not hasattr(self, f"_{name}"):
|
||||
setattr(self, f"_{name}", default_value)
|
||||
return getattr(self, f"_{name}")
|
||||
|
||||
def fset(self, value, name=name, signal=signal):
|
||||
setattr(self, f"_{name}", value)
|
||||
if signal:
|
||||
getattr(self, f"{name}Changed").emit(value)
|
||||
|
||||
kws = {"notify": signal} if signal else {"constant": True}
|
||||
|
||||
return pyqtProperty(qt_type, fget=fget, fset=fset, **kws)
|
||||
|
||||
|
||||
class User(ListItem):
|
||||
roles = ("userId", "displayName", "avatarUrl", "statusMessage")
|
||||
|
||||
displayNameChanged = pyqtSignal("QVariant")
|
||||
avatarUrlChanged = pyqtSignal("QVariant")
|
||||
statusMessageChanged = pyqtSignal(str)
|
||||
|
||||
userId = prop(str, "userId")
|
||||
displayName = prop("QVariant", "displayName", displayNameChanged)
|
||||
avatarUrl = prop(str, "avatarUrl", avatarUrlChanged)
|
||||
statusMessage = prop(str, "statusMessage", statusMessageChanged, "")
|
||||
|
||||
|
||||
class Room(ListItem):
|
||||
roles = ("roomId", "displayName", "topic", "typingUsers")
|
||||
|
||||
displayNameChanged = pyqtSignal("QVariant")
|
||||
topicChanged = pyqtSignal(str)
|
||||
typingUsersChanged = pyqtSignal("QVariantList")
|
||||
|
||||
roomId = prop(str, "roomId")
|
||||
displayName = prop(str, "displayName", displayNameChanged)
|
||||
topic = prop(str, "topic", topicChanged, "")
|
||||
typingUsers = prop(list, "typingUsers", typingUsersChanged, [])
|
||||
|
||||
|
||||
class RoomEvent(ListItem):
|
||||
roles = ("type", "dateTime", "dict", "isLocalEcho")
|
||||
|
||||
type = prop(str, "type")
|
||||
dateTime = prop("QVariant", "dateTime")
|
||||
dict = prop("QVariantMap", "dict")
|
||||
isLocalEcho = prop(bool, "isLocalEcho", None, False)
|
||||
|
@@ -4,27 +4,25 @@ from typing import (
|
||||
Sequence, Tuple, Union
|
||||
)
|
||||
|
||||
from namedlist import namedlist
|
||||
from PyQt5.QtCore import (
|
||||
QAbstractListModel, QModelIndex, QObject, Qt, pyqtProperty, pyqtSignal,
|
||||
pyqtSlot
|
||||
)
|
||||
|
||||
NewValue = Union[Mapping[str, Any], Sequence]
|
||||
ReturnItem = Dict[str, Any]
|
||||
from .items import ListItem
|
||||
|
||||
NewItem = Union[ListItem, Mapping[str, Any], Sequence]
|
||||
|
||||
|
||||
class ListModel(QAbstractListModel):
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self,
|
||||
initial_data: Optional[List[NewValue]] = None,
|
||||
initial_data: Optional[List[NewItem]] = None,
|
||||
container: Callable[..., MutableSequence] = list,
|
||||
parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._ref_namedlist = None
|
||||
self._roles: Tuple[str, ...] = ()
|
||||
self._data: MutableSequence = container()
|
||||
self._data: MutableSequence[ListItem] = container()
|
||||
|
||||
if initial_data:
|
||||
self.extend(initial_data)
|
||||
@@ -50,57 +48,64 @@ class ListModel(QAbstractListModel):
|
||||
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)}
|
||||
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 self._data[index.row()][role - Qt.UserRole - 1]
|
||||
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: NewValue) -> Any:
|
||||
if isinstance(value, Mapping):
|
||||
if not self._ref_namedlist:
|
||||
self._ref_namedlist = namedlist("ListItem", value.keys())
|
||||
self._roles = tuple(value.keys())
|
||||
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 self._ref_namedlist(**value) # type: ignore
|
||||
return type(self._data[0])(**value)
|
||||
|
||||
if isinstance(value, Sequence):
|
||||
if not self._ref_namedlist:
|
||||
try:
|
||||
self._ref_namedlist = namedlist(
|
||||
value.__class__.__name__, value._fields # type: ignore
|
||||
)
|
||||
self._roles = tuple(value._fields) # type: ignore
|
||||
except AttributeError:
|
||||
raise TypeError(
|
||||
"Need a mapping/dict, namedtuple or namedlist as "
|
||||
"first value to set allowed keys/fields."
|
||||
)
|
||||
if not self._data and isinstance(value, Mapping):
|
||||
raise NotImplementedError("First item must be set from Python")
|
||||
|
||||
return self._ref_namedlist(*value) # type: ignore
|
||||
if self._data and isinstance(value, type(self._data[0])):
|
||||
return value
|
||||
|
||||
if not self._data and isinstance(value, ListItem):
|
||||
return value
|
||||
|
||||
raise TypeError("Value must be a mapping or sequence.")
|
||||
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: # pylint: disable=arguments-differ
|
||||
def count(self) -> int:
|
||||
return self.rowCount()
|
||||
|
||||
|
||||
@pyqtSlot(int, result="QVariantMap")
|
||||
def get(self, index: int) -> ReturnItem:
|
||||
return self._data[index]._asdict()
|
||||
@pyqtSlot(int, result="QVariant")
|
||||
def get(self, index: int) -> ListItem:
|
||||
return self._data[index]
|
||||
|
||||
|
||||
@pyqtSlot(str, "QVariant", result=int)
|
||||
@@ -109,17 +114,17 @@ class ListModel(QAbstractListModel):
|
||||
if getattr(item, prop) == is_value:
|
||||
return i
|
||||
|
||||
raise ValueError(f"No {type(self._ref_namedlist)} in list with "
|
||||
raise ValueError(f"No item in model data with "
|
||||
f"property {prop!r} set to {is_value!r}.")
|
||||
|
||||
|
||||
@pyqtSlot(str, "QVariant", result="QVariantMap")
|
||||
def getWhere(self, prop: str, is_value: Any) -> ReturnItem:
|
||||
@pyqtSlot(str, "QVariant", result="QVariant")
|
||||
def getWhere(self, prop: str, is_value: Any) -> ListItem:
|
||||
return self.get(self.indexWhere(prop, is_value))
|
||||
|
||||
|
||||
@pyqtSlot(int, list)
|
||||
def insert(self, index: int, value: NewValue) -> None:
|
||||
@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)
|
||||
@@ -127,19 +132,44 @@ class ListModel(QAbstractListModel):
|
||||
self.changed.emit()
|
||||
|
||||
|
||||
@pyqtSlot(list)
|
||||
def append(self, value: NewValue) -> None:
|
||||
@pyqtSlot("QVariantMap")
|
||||
def append(self, value: NewItem) -> None:
|
||||
self.insert(self.rowCount(), value)
|
||||
|
||||
|
||||
@pyqtSlot(list)
|
||||
def extend(self, values: Iterable[NewValue]) -> None:
|
||||
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:
|
||||
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: NewValue) -> None:
|
||||
def set(self, index: int, value: NewItem) -> None:
|
||||
qidx = QAbstractListModel.index(self, index, 0)
|
||||
value = self._convert_new_value(value)
|
||||
self._data[index] = value
|
||||
@@ -149,16 +179,16 @@ class ListModel(QAbstractListModel):
|
||||
|
||||
@pyqtSlot(int, str, "QVariant")
|
||||
def setProperty(self, index: int, prop: str, value: Any) -> None:
|
||||
self._data[index][self._roles.index(prop)] = value
|
||||
setattr(self._data[index], prop, value)
|
||||
qidx = QAbstractListModel.index(self, index, 0)
|
||||
self.dataChanged.emit(qidx, qidx, self.roleNames())
|
||||
self.changed.emit()
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@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 \
|
||||
@@ -186,7 +216,7 @@ class ListModel(QAbstractListModel):
|
||||
|
||||
|
||||
@pyqtSlot(int)
|
||||
def remove(self, index: int) -> None: # pylint: disable=arguments-differ
|
||||
def remove(self, index: int) -> None:
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
del self._data[index]
|
||||
self.endRemoveRows()
|
||||
|
Reference in New Issue
Block a user