Show joined rooms, delete left rooms

To make the models update correctly in QML:
- ListModel and _QtModel merged
- Return a ListModelMap QObject from properties instead of
  a DefaultDict → QVariantMap
This commit is contained in:
miruka 2019-04-12 13:18:46 -04:00
parent 381c6b5b1c
commit 30514fb7db
12 changed files with 177 additions and 101 deletions

View File

@ -6,7 +6,7 @@ from concurrent.futures import Future, ThreadPoolExecutor
from threading import Event from threading import Event
from typing import Callable, DefaultDict, Dict from typing import Callable, DefaultDict, Dict
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
import nio import nio
import nio.responses as nr import nio.responses as nr
@ -27,6 +27,11 @@ def futurize(func: Callable) -> Callable:
class Client(QObject): class Client(QObject):
roomInvited = pyqtSignal(str)
roomJoined = pyqtSignal(str)
roomLeft = pyqtSignal(str)
def __init__(self, hostname: str, username: str, device_id: str = "" def __init__(self, hostname: str, username: str, device_id: str = ""
) -> None: ) -> None:
super().__init__() super().__init__()
@ -48,7 +53,12 @@ class Client(QObject):
def __repr__(self) -> str: def __repr__(self) -> str:
return "%s(host=%r, port=%r, user_id=%r)" % \ return "%s(host=%r, port=%r, user_id=%r)" % \
(type(self).__name__, self.host, self.port, self.nio.user_id) (type(self).__name__, self.host, self.port, self.userID)
@pyqtProperty(str, constant=True)
def userID(self) -> str:
return self.nio.user_id
@pyqtSlot(str) @pyqtSlot(str)
@ -81,13 +91,24 @@ class Client(QObject):
@futurize @futurize
def startSyncing(self) -> None: def startSyncing(self) -> None:
while True: while True:
self.net.talk(self.nio.sync, timeout=10) self._on_sync(self.net.talk(self.nio.sync, timeout=10))
if self._stop_sync.is_set(): if self._stop_sync.is_set():
self._stop_sync.clear() self._stop_sync.clear()
break break
def _on_sync(self, response: nr.SyncResponse) -> None:
for room_id in response.rooms.invite:
self.roomInvited.emit(room_id)
for room_id in response.rooms.join:
self.roomJoined.emit(room_id)
for room_id in response.rooms.left:
self.roomLeft.emit(room_id)
@pyqtSlot(str, str, result="QVariantMap") @pyqtSlot(str, str, result="QVariantMap")
def getUser(self, room_id: str, user_id: str) -> Dict[str, str]: def getUser(self, room_id: str, user_id: str) -> Dict[str, str]:
try: try:

View File

@ -61,7 +61,7 @@ class ClientManager(QObject):
def _on_connected(self, client: Client) -> None: def _on_connected(self, client: Client) -> None:
self.clients[client.nio.user_id] = client self.clients[client.userID] = client
self.clientAdded.emit(client) self.clientAdded.emit(client)
@ -127,7 +127,7 @@ class ClientManager(QObject):
def configAdd(self, client: Client) -> None: def configAdd(self, client: Client) -> None:
self._write_config({ self._write_config({
**self.configAccounts(), **self.configAccounts(),
**{client.nio.user_id: { **{client.userID: {
"hostname": client.nio.host, "hostname": client.nio.host,
"token": client.nio.access_token, "token": client.nio.access_token,
"device_id": client.nio.device_id, "device_id": client.nio.device_id,

View File

@ -1,6 +1,7 @@
# Copyright 2019 miruka # Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3. # This file is part of harmonyqml, licensed under GPLv3.
from .list_model import ListModel, _QtListModel from .list_model import ListModel
from .list_model_map import ListModelMap
from .qml_models import QMLModels from .qml_models import QMLModels
from . import enums, items from . import enums, items

View File

@ -1,6 +1,7 @@
import logging import logging
from collections.abc import MutableSequence from typing import (
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
)
from namedlist import namedlist from namedlist import namedlist
from PyQt5.QtCore import ( from PyQt5.QtCore import (
@ -10,13 +11,39 @@ from PyQt5.QtCore import (
NewValue = Union[Mapping[str, Any], Sequence] NewValue = Union[Mapping[str, Any], Sequence]
class _QtListModel(QAbstractListModel): class ListModel(QAbstractListModel):
def __init__(self) -> None: def __init__(self, initial_data: Optional[List[NewValue]] = None) -> None:
super().__init__() super().__init__()
self._ref_namedlist = None self._ref_namedlist = None
self._roles: Tuple[str, ...] = () self._roles: Tuple[str, ...] = ()
self._list: list = [] self._list: list = []
self._update_count: int = 0
if initial_data:
self.extend(initial_data)
def __repr__(self) -> str:
return "%s[%s]" % (type(self).__name__,
", ".join((repr(i) for i in self)))
def __getitem__(self, index):
return self._list[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()
def roleNames(self) -> Dict[int, bytes]: def roleNames(self) -> Dict[int, bytes]:
return {Qt.UserRole + i: bytes(f, "utf-8") return {Qt.UserRole + i: bytes(f, "utf-8")
for i, f in enumerate(self._roles, 1)} for i, f in enumerate(self._roles, 1)}
@ -60,6 +87,11 @@ class _QtListModel(QAbstractListModel):
raise TypeError("Value must be a mapping or sequence.") raise TypeError("Value must be a mapping or sequence.")
@pyqtProperty(int, constant=True)
def count(self) -> int: # pylint: disable=arguments-differ
return self.rowCount()
@pyqtSlot(int, result="QVariantMap") @pyqtSlot(int, result="QVariantMap")
def get(self, index: int) -> Dict[str, Any]: def get(self, index: int) -> Dict[str, Any]:
return self._list[index]._asdict() return self._list[index]._asdict()
@ -81,11 +113,7 @@ class _QtListModel(QAbstractListModel):
self.beginInsertRows(QModelIndex(), index, index) self.beginInsertRows(QModelIndex(), index, index)
self._list.insert(index, value) self._list.insert(index, value)
self.endInsertRows() self.endInsertRows()
self._update_count += 1
@pyqtProperty(int)
def count(self) -> int:
return self.rowCount()
@pyqtSlot(list) @pyqtSlot(list)
@ -93,19 +121,27 @@ class _QtListModel(QAbstractListModel):
self.insert(self.rowCount(), value) self.insert(self.rowCount(), value)
@pyqtSlot(list)
def extend(self, values: Iterable[NewValue]) -> None:
for val in values:
self.append(val)
@pyqtSlot(int, list) @pyqtSlot(int, list)
def set(self, index: int, value: NewValue) -> None: def set(self, index: int, value: NewValue) -> None:
qidx = self.index(index, 0) qidx = QAbstractListModel.index(index, 0)
value = self._convert_new_value(value) value = self._convert_new_value(value)
self._list[index] = value self._list[index] = value
self.dataChanged.emit(qidx, qidx, self.roleNames()) self.dataChanged.emit(qidx, qidx, self.roleNames())
self._update_count += 1
@pyqtSlot(int, str, "QVariant") @pyqtSlot(int, str, "QVariant")
def setProperty(self, index: int, prop: str, value: Any) -> None: def setProperty(self, index: int, prop: str, value: Any) -> None:
self._list[index][self._roles.index(prop)] = value self._list[index][self._roles.index(prop)] = value
qidx = self.index(index, 0) qidx = QAbstractListModel.index(index, 0)
self.dataChanged.emit(qidx, qidx, self.roleNames()) self.dataChanged.emit(qidx, qidx, self.roleNames())
self._update_count += 1
# pylint: disable=invalid-name # pylint: disable=invalid-name
@ -135,13 +171,15 @@ class _QtListModel(QAbstractListModel):
self._list[to:to] = cut self._list[to:to] = cut
self.endMoveRows() self.endMoveRows()
self._update_count += 1
@pyqtSlot(int) @pyqtSlot(int)
def remove(self, index: int) -> None: def remove(self, index: int) -> None: # pylint: disable=arguments-differ
self.beginRemoveRows(QModelIndex(), index, index) self.beginRemoveRows(QModelIndex(), index, index)
del self._list[index] del self._list[index]
self.endRemoveRows() self.endRemoveRows()
self._update_count += 1
@pyqtSlot() @pyqtSlot()
@ -150,56 +188,9 @@ class _QtListModel(QAbstractListModel):
self.beginRemoveRows(QModelIndex(), 0, self.rowCount()) self.beginRemoveRows(QModelIndex(), 0, self.rowCount())
self._list.clear() self._list.clear()
self.endRemoveRows() self.endRemoveRows()
self._update_count += 1
class ListModel(MutableSequence): @pyqtProperty(int, constant=True)
def __init__(self, initial_data: Optional[List[NewValue]] = None) -> None: def reloadThis(self):
super().__init__() return self._update_count
self.qt_model = _QtListModel()
if initial_data:
self.extend(initial_data)
def __repr__(self) -> str:
return "%s[%s]" % (type(self).__name__,
", ".join((repr(i) for i in self)))
def __getitem__(self, index):
# pylint: disable=protected-access
return self.qt_model._list[index]
def __setitem__(self, index, value) -> None:
self.qt_model.set(index, value)
def __delitem__(self, index) -> None:
self.qt_model.remove(index)
def __len__(self) -> int:
return self.qt_model.rowCount()
def insert(self, index: int, value: NewValue) -> None:
self.qt_model.insert(index, value)
def indexWhere(self, prop: str, is_value: Any) -> int:
return self.qt_model.indexWhere(prop, is_value)
def setProperty(self, index: int, prop: str, value: Any) -> None:
"Set role of the item at *index* to *value*."
self.qt_model.setProperty(index, prop, value)
# pylint: disable=invalid-name
def move(self, from_: int, to: int, n: int = 1) -> None:
"Move *n* items *from_* index *to* another."
self.qt_model.move(from_, to, n)
def clear(self) -> None:
self.qt_model.clear()

View File

@ -0,0 +1,36 @@
from typing import Any, DefaultDict
from PyQt5.QtCore import QObject, pyqtSlot
from .list_model import ListModel
class ListModelMap(QObject):
def __init__(self) -> None:
super().__init__()
self.dict: DefaultDict[Any, ListModel] = DefaultDict(ListModel)
@pyqtSlot(str, result="QVariant")
def get(self, key) -> ListModel:
return self.dict[key]
def __getitem__(self, key) -> ListModel:
return self.dict[key]
def __setitem__(self, key, value: ListModel) -> None:
self.dict[key] = value
def __detitem__(self, key) -> None:
del self.dict[key]
def __iter__(self):
return iter(self.dict)
def __len__(self) -> int:
return len(self.dict)

View File

@ -1,31 +1,33 @@
# Copyright 2019 miruka # Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3. # This file is part of harmonyqml, licensed under GPLv3.
from typing import DefaultDict, Dict from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
from PyQt5.QtCore import QObject, pyqtProperty from .list_model import ListModel
from .list_model_map import ListModelMap
from .list_model import ListModel, _QtListModel
class QMLModels(QObject): class QMLModels(QObject):
roomsChanged = pyqtSignal()
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self._accounts: ListModel = ListModel() self._accounts: ListModel = ListModel()
self._rooms: DefaultDict[str, ListModel] = DefaultDict(ListModel) self._rooms: ListModelMap = ListModelMap()
self._messages: DefaultDict[str, ListModel] = DefaultDict(ListModel) self._messages: ListModelMap = ListModelMap()
@pyqtProperty(_QtListModel, constant=True) @pyqtProperty(ListModel, constant=True)
def accounts(self) -> _QtListModel: def accounts(self):
return self._accounts.qt_model return self._accounts
@pyqtProperty("QVariantMap", constant=True) @pyqtProperty("QVariant", notify=roomsChanged)
def rooms(self) -> Dict[str, _QtListModel]: def rooms(self):
return {user_id: l.qt_model for user_id, l in self._rooms.items()} return self._rooms
@pyqtProperty("QVariantMap", constant=True) @pyqtProperty("QVariant", constant=True)
def messages(self) -> Dict[str, _QtListModel]: def messages(self):
return {room_id: l.qt_model for room_id, l in self._messages.items()} return self._messages

View File

@ -5,29 +5,54 @@ from PyQt5.QtCore import QObject
from .backend import Backend from .backend import Backend
from .client import Client from .client import Client
from .model.items import User from .model.items import User, Room
class SignalManager(QObject): class SignalManager(QObject):
def __init__(self, backend: Backend) -> None: def __init__(self, backend: Backend) -> None:
super().__init__() super().__init__()
self.backend = backend self.backend = backend
self.connectAll()
cm = self.backend.clientManager
def connectAll(self) -> None: cm.clientAdded.connect(self.onClientAdded)
be = self.backend cm.clientDeleted.connect(self.onClientDeleted)
be.clientManager.clientAdded.connect(self.onClientAdded)
be.clientManager.clientDeleted.connect(self.onClientDeleted)
def onClientAdded(self, client: Client) -> None: def onClientAdded(self, client: Client) -> None:
self.connectClient(client)
self.backend.models.accounts.append(User( self.backend.models.accounts.append(User(
user_id = client.nio.user_id, user_id = client.userID,
display_name = client.nio.user_id.lstrip("@").split(":")[0], display_name = client.userID.lstrip("@").split(":")[0],
)) ))
def onClientDeleted(self, user_id: str) -> None: def onClientDeleted(self, user_id: str) -> None:
accs = self.backend.models.accounts accs = self.backend.models.accounts
del accs[accs.indexWhere("user_id", user_id)] del accs[accs.indexWhere("user_id", user_id)]
def connectClient(self, client: Client) -> None:
for sig_name in ("roomInvited", "roomJoined", "roomLeft"):
sig = getattr(client, sig_name)
on_sig = getattr(self, f"on{sig_name[0].upper()}{sig_name[1:]}")
sig.connect(lambda room_id, o=on_sig, c=client: o(c, room_id))
def onRoomInvited(self, client: Client, room_id: str) -> None:
pass # TODO
def onRoomJoined(self, client: Client, room_id: str) -> None:
room = client.nio.rooms[room_id]
name = room.name or room.canonical_alias or room.group_name()
self.backend.models.rooms[client.userID].append(Room(
room_id = room_id,
display_name = name,
description = getattr(room, "topic", ""), # FIXME: outside init
))
def onRoomLeft(self, client: Client, room_id: str) -> None:
rooms = self.backend.models.rooms[client.userID]
del rooms[rooms.indexWhere("room_id", room_id)]

View File

@ -13,7 +13,7 @@ Rectangle {
ListView { ListView {
id: messageListView id: messageListView
anchors.fill: parent anchors.fill: parent
model: Backend.models.messages[chatPage.room.room_id] model: Backend.models.messages.get(chatPage.room.room_id)
delegate: MessageDelegate {} delegate: MessageDelegate {}
//highlight: Rectangle {color: "lightsteelblue"; radius: 5} //highlight: Rectangle {color: "lightsteelblue"; radius: 5}

View File

@ -71,7 +71,7 @@ ColumnLayout {
Layout.minimumHeight: Layout.minimumHeight:
roomList.visible ? roomList.visible ?
roomList.contentHeight + roomList.anchors.margins * 2 : 800 :
0 0
Layout.maximumHeight: Layout.minimumHeight Layout.maximumHeight: Layout.minimumHeight

View File

@ -38,8 +38,8 @@ MouseArea {
} }
Base.HLabel { Base.HLabel {
function get_text() { function get_text() {
var msgs = Backend.models.messages[room_id] var msgs = Backend.models.messages.get(room_id)
if (msgs.count < 1) { return "" } if (! msgs || msgs.count < 1) { return "" }
var msg = msgs.get(-1) var msg = msgs.get(-1)
var color_ = (msg.sender_id === roomList.user_id ? var color_ = (msg.sender_id === roomList.user_id ?
@ -54,7 +54,7 @@ MouseArea {
id: subtitleLabel id: subtitleLabel
visible: text !== "" visible: text !== ""
text: Backend.models.messages[room_id].reloadThis, get_text() text: Backend.models.messages.get(room_id).reloadThis, get_text()
textFormat: Text.StyledText textFormat: Text.StyledText
font.pixelSize: smallSize font.pixelSize: smallSize

View File

@ -21,6 +21,6 @@ ListView {
id: "roomList" id: "roomList"
spacing: 8 spacing: 8
model: Backend.models.rooms[for_user_id] model: Backend.models.rooms.get(for_user_id)
delegate: RoomDelegate {} delegate: RoomDelegate {}
} }