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 typing import Callable, DefaultDict, Dict
from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
import nio
import nio.responses as nr
@ -27,6 +27,11 @@ def futurize(func: Callable) -> Callable:
class Client(QObject):
roomInvited = pyqtSignal(str)
roomJoined = pyqtSignal(str)
roomLeft = pyqtSignal(str)
def __init__(self, hostname: str, username: str, device_id: str = ""
) -> None:
super().__init__()
@ -48,7 +53,12 @@ class Client(QObject):
def __repr__(self) -> str:
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)
@ -81,13 +91,24 @@ class Client(QObject):
@futurize
def startSyncing(self) -> None:
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():
self._stop_sync.clear()
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")
def getUser(self, room_id: str, user_id: str) -> Dict[str, str]:
try:

View File

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

View File

@ -1,6 +1,7 @@
# Copyright 2019 miruka
# 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 . import enums, items

View File

@ -1,6 +1,7 @@
import logging
from collections.abc import MutableSequence
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
from typing import (
Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
)
from namedlist import namedlist
from PyQt5.QtCore import (
@ -10,13 +11,39 @@ from PyQt5.QtCore import (
NewValue = Union[Mapping[str, Any], Sequence]
class _QtListModel(QAbstractListModel):
def __init__(self) -> None:
class ListModel(QAbstractListModel):
def __init__(self, initial_data: Optional[List[NewValue]] = None) -> None:
super().__init__()
self._ref_namedlist = None
self._roles: Tuple[str, ...] = ()
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]:
return {Qt.UserRole + i: bytes(f, "utf-8")
for i, f in enumerate(self._roles, 1)}
@ -60,6 +87,11 @@ class _QtListModel(QAbstractListModel):
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")
def get(self, index: int) -> Dict[str, Any]:
return self._list[index]._asdict()
@ -81,11 +113,7 @@ class _QtListModel(QAbstractListModel):
self.beginInsertRows(QModelIndex(), index, index)
self._list.insert(index, value)
self.endInsertRows()
@pyqtProperty(int)
def count(self) -> int:
return self.rowCount()
self._update_count += 1
@pyqtSlot(list)
@ -93,19 +121,27 @@ class _QtListModel(QAbstractListModel):
self.insert(self.rowCount(), value)
@pyqtSlot(list)
def extend(self, values: Iterable[NewValue]) -> None:
for val in values:
self.append(val)
@pyqtSlot(int, list)
def set(self, index: int, value: NewValue) -> None:
qidx = self.index(index, 0)
qidx = QAbstractListModel.index(index, 0)
value = self._convert_new_value(value)
self._list[index] = value
self.dataChanged.emit(qidx, qidx, self.roleNames())
self._update_count += 1
@pyqtSlot(int, str, "QVariant")
def setProperty(self, index: int, prop: str, value: Any) -> None:
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._update_count += 1
# pylint: disable=invalid-name
@ -135,13 +171,15 @@ class _QtListModel(QAbstractListModel):
self._list[to:to] = cut
self.endMoveRows()
self._update_count += 1
@pyqtSlot(int)
def remove(self, index: int) -> None:
def remove(self, index: int) -> None: # pylint: disable=arguments-differ
self.beginRemoveRows(QModelIndex(), index, index)
del self._list[index]
self.endRemoveRows()
self._update_count += 1
@pyqtSlot()
@ -150,56 +188,9 @@ class _QtListModel(QAbstractListModel):
self.beginRemoveRows(QModelIndex(), 0, self.rowCount())
self._list.clear()
self.endRemoveRows()
self._update_count += 1
class ListModel(MutableSequence):
def __init__(self, initial_data: Optional[List[NewValue]] = None) -> None:
super().__init__()
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()
@pyqtProperty(int, constant=True)
def reloadThis(self):
return self._update_count

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
# 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, _QtListModel
from .list_model import ListModel
from .list_model_map import ListModelMap
class QMLModels(QObject):
roomsChanged = pyqtSignal()
def __init__(self) -> None:
super().__init__()
self._accounts: ListModel = ListModel()
self._rooms: DefaultDict[str, ListModel] = DefaultDict(ListModel)
self._messages: DefaultDict[str, ListModel] = DefaultDict(ListModel)
self._accounts: ListModel = ListModel()
self._rooms: ListModelMap = ListModelMap()
self._messages: ListModelMap = ListModelMap()
@pyqtProperty(_QtListModel, constant=True)
def accounts(self) -> _QtListModel:
return self._accounts.qt_model
@pyqtProperty(ListModel, constant=True)
def accounts(self):
return self._accounts
@pyqtProperty("QVariantMap", constant=True)
def rooms(self) -> Dict[str, _QtListModel]:
return {user_id: l.qt_model for user_id, l in self._rooms.items()}
@pyqtProperty("QVariant", notify=roomsChanged)
def rooms(self):
return self._rooms
@pyqtProperty("QVariantMap", constant=True)
def messages(self) -> Dict[str, _QtListModel]:
return {room_id: l.qt_model for room_id, l in self._messages.items()}
@pyqtProperty("QVariant", constant=True)
def messages(self):
return self._messages

View File

@ -5,29 +5,54 @@ from PyQt5.QtCore import QObject
from .backend import Backend
from .client import Client
from .model.items import User
from .model.items import User, Room
class SignalManager(QObject):
def __init__(self, backend: Backend) -> None:
super().__init__()
self.backend = backend
self.connectAll()
def connectAll(self) -> None:
be = self.backend
be.clientManager.clientAdded.connect(self.onClientAdded)
be.clientManager.clientDeleted.connect(self.onClientDeleted)
cm = self.backend.clientManager
cm.clientAdded.connect(self.onClientAdded)
cm.clientDeleted.connect(self.onClientDeleted)
def onClientAdded(self, client: Client) -> None:
self.connectClient(client)
self.backend.models.accounts.append(User(
user_id = client.nio.user_id,
display_name = client.nio.user_id.lstrip("@").split(":")[0],
user_id = client.userID,
display_name = client.userID.lstrip("@").split(":")[0],
))
def onClientDeleted(self, user_id: str) -> None:
accs = self.backend.models.accounts
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 {
id: messageListView
anchors.fill: parent
model: Backend.models.messages[chatPage.room.room_id]
model: Backend.models.messages.get(chatPage.room.room_id)
delegate: MessageDelegate {}
//highlight: Rectangle {color: "lightsteelblue"; radius: 5}

View File

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

View File

@ -5,7 +5,7 @@ import QtQuick.Layouts 1.4
ListView {
id: "accountList"
spacing: 8
model: Backend.models.accounts
model: Backend.models.accounts
delegate: AccountDelegate {}
clip: true
}

View File

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

View File

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