diff --git a/harmonyqml/backend/client.py b/harmonyqml/backend/client.py index a85ce5d9..b9dbc936 100644 --- a/harmonyqml/backend/client.py +++ b/harmonyqml/backend/client.py @@ -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: diff --git a/harmonyqml/backend/client_manager.py b/harmonyqml/backend/client_manager.py index a5f8fc26..a6a546b5 100644 --- a/harmonyqml/backend/client_manager.py +++ b/harmonyqml/backend/client_manager.py @@ -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, diff --git a/harmonyqml/backend/model/__init__.py b/harmonyqml/backend/model/__init__.py index 93ad0d1e..5ed4936a 100644 --- a/harmonyqml/backend/model/__init__.py +++ b/harmonyqml/backend/model/__init__.py @@ -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 diff --git a/harmonyqml/backend/model/list_model.py b/harmonyqml/backend/model/list_model.py index 48f7d404..f967a303 100644 --- a/harmonyqml/backend/model/list_model.py +++ b/harmonyqml/backend/model/list_model.py @@ -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 diff --git a/harmonyqml/backend/model/list_model_map.py b/harmonyqml/backend/model/list_model_map.py new file mode 100644 index 00000000..4528e3e4 --- /dev/null +++ b/harmonyqml/backend/model/list_model_map.py @@ -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) diff --git a/harmonyqml/backend/model/qml_models.py b/harmonyqml/backend/model/qml_models.py index 26be7052..7e4f927b 100644 --- a/harmonyqml/backend/model/qml_models.py +++ b/harmonyqml/backend/model/qml_models.py @@ -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 diff --git a/harmonyqml/backend/signal_manager.py b/harmonyqml/backend/signal_manager.py index 03f833f9..8b8791e5 100644 --- a/harmonyqml/backend/signal_manager.py +++ b/harmonyqml/backend/signal_manager.py @@ -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)] diff --git a/harmonyqml/components/chat/MessageList.qml b/harmonyqml/components/chat/MessageList.qml index 402ed40f..3c33812c 100644 --- a/harmonyqml/components/chat/MessageList.qml +++ b/harmonyqml/components/chat/MessageList.qml @@ -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} diff --git a/harmonyqml/components/side_pane/AccountDelegate.qml b/harmonyqml/components/side_pane/AccountDelegate.qml index 00d142a2..1d90baf0 100644 --- a/harmonyqml/components/side_pane/AccountDelegate.qml +++ b/harmonyqml/components/side_pane/AccountDelegate.qml @@ -71,7 +71,7 @@ ColumnLayout { Layout.minimumHeight: roomList.visible ? - roomList.contentHeight + roomList.anchors.margins * 2 : + 800 : 0 Layout.maximumHeight: Layout.minimumHeight diff --git a/harmonyqml/components/side_pane/AccountList.qml b/harmonyqml/components/side_pane/AccountList.qml index e9f455c5..a589ea05 100644 --- a/harmonyqml/components/side_pane/AccountList.qml +++ b/harmonyqml/components/side_pane/AccountList.qml @@ -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 } diff --git a/harmonyqml/components/side_pane/RoomDelegate.qml b/harmonyqml/components/side_pane/RoomDelegate.qml index 5cd072da..d9b497f7 100644 --- a/harmonyqml/components/side_pane/RoomDelegate.qml +++ b/harmonyqml/components/side_pane/RoomDelegate.qml @@ -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 diff --git a/harmonyqml/components/side_pane/RoomList.qml b/harmonyqml/components/side_pane/RoomList.qml index e42842b9..3b3c733e 100644 --- a/harmonyqml/components/side_pane/RoomList.qml +++ b/harmonyqml/components/side_pane/RoomList.qml @@ -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 {} }