Reorganize backend files, show accounts in UI
This commit is contained in:
6
harmonyqml/backend/model/__init__.py
Normal file
6
harmonyqml/backend/model/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
from .list_model import ListModel, _QtListModel
|
||||
from .qml_models import QMLModels
|
||||
from . import enums, items
|
31
harmonyqml/backend/model/enums.py
Normal file
31
harmonyqml/backend/model/enums.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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"
|
34
harmonyqml/backend/model/items.py
Normal file
34
harmonyqml/backend/model/items.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from PyQt5.QtCore import QDateTime
|
||||
|
||||
from .enums import Activity, MessageKind, Presence
|
||||
|
||||
|
||||
class User(NamedTuple):
|
||||
user_id: str
|
||||
display_name: str
|
||||
avatar_url: Optional[str] = None
|
||||
status_message: Optional[str] = None
|
||||
|
||||
|
||||
class Room(NamedTuple):
|
||||
room_id: str
|
||||
display_name: str
|
||||
description: str = ""
|
||||
unread_messages: int = 0
|
||||
presence: Presence = Presence.none
|
||||
activity: Activity = Activity.none
|
||||
last_activity_timestamp_ms: Optional[int] = None
|
||||
avatar_url: Optional[str] = None
|
||||
|
||||
|
||||
class Message(NamedTuple):
|
||||
sender_id: str
|
||||
date_time: QDateTime
|
||||
content: str
|
||||
kind: MessageKind = MessageKind.text
|
||||
sender_avatar: Optional[str] = None
|
205
harmonyqml/backend/model/list_model.py
Normal file
205
harmonyqml/backend/model/list_model.py
Normal file
@@ -0,0 +1,205 @@
|
||||
import logging
|
||||
from collections.abc import MutableSequence
|
||||
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
|
||||
|
||||
from namedlist import namedlist
|
||||
from PyQt5.QtCore import (
|
||||
QAbstractListModel, QModelIndex, Qt, pyqtProperty, pyqtSlot
|
||||
)
|
||||
|
||||
NewValue = Union[Mapping[str, Any], Sequence]
|
||||
|
||||
|
||||
class _QtListModel(QAbstractListModel):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._ref_namedlist = None
|
||||
self._roles: Tuple[str, ...] = ()
|
||||
self._list: list = []
|
||||
|
||||
def roleNames(self) -> Dict[int, bytes]:
|
||||
return {Qt.UserRole + i: bytes(f, "utf-8")
|
||||
for i, f in enumerate(self._roles, 1)}
|
||||
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
if role <= Qt.UserRole:
|
||||
return None
|
||||
|
||||
return self._list[index.row()][role - Qt.UserRole - 1]
|
||||
|
||||
|
||||
def rowCount(self, _: QModelIndex = QModelIndex()) -> int:
|
||||
return len(self._list)
|
||||
|
||||
|
||||
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())
|
||||
|
||||
return self._ref_namedlist(**value) # type: ignore
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
return self._ref_namedlist(*value) # type: ignore
|
||||
|
||||
|
||||
raise TypeError("Value must be a mapping or sequence.")
|
||||
|
||||
|
||||
@pyqtSlot(int, result="QVariantMap")
|
||||
def get(self, index: int) -> Dict[str, Any]:
|
||||
return self._list[index]._asdict()
|
||||
|
||||
|
||||
@pyqtSlot(str, "QVariant", result=int)
|
||||
def indexWhere(self, prop: str, is_value: Any) -> int:
|
||||
for i, item in enumerate(self._list):
|
||||
if getattr(item, prop) == is_value:
|
||||
return i
|
||||
|
||||
raise ValueError(f"No {type(self._ref_namedlist)} in list with "
|
||||
f"property {prop!r} set to {is_value!r}.")
|
||||
|
||||
|
||||
@pyqtSlot(int, list)
|
||||
def insert(self, index: int, value: NewValue) -> None:
|
||||
value = self._convert_new_value(value)
|
||||
self.beginInsertRows(QModelIndex(), index, index)
|
||||
self._list.insert(index, value)
|
||||
self.endInsertRows()
|
||||
|
||||
|
||||
@pyqtProperty(int)
|
||||
def count(self) -> int:
|
||||
return self.rowCount()
|
||||
|
||||
|
||||
@pyqtSlot(list)
|
||||
def append(self, value: NewValue) -> None:
|
||||
self.insert(self.rowCount(), value)
|
||||
|
||||
|
||||
@pyqtSlot(int, list)
|
||||
def set(self, index: int, value: NewValue) -> None:
|
||||
qidx = self.index(index, 0)
|
||||
value = self._convert_new_value(value)
|
||||
self._list[index] = value
|
||||
self.dataChanged.emit(qidx, qidx, self.roleNames())
|
||||
|
||||
|
||||
@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)
|
||||
self.dataChanged.emit(qidx, qidx, self.roleNames())
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@pyqtSlot(int, int)
|
||||
@pyqtSlot(int, int, int)
|
||||
def move(self, from_: int, to: int, n: int = 1) -> None:
|
||||
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:
|
||||
logging.warning("No need for move or out of range")
|
||||
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._list[from_:last]
|
||||
del self._list[from_:last]
|
||||
self._list[to:to] = cut
|
||||
|
||||
self.endMoveRows()
|
||||
|
||||
|
||||
@pyqtSlot(int)
|
||||
def remove(self, index: int) -> None:
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
del self._list[index]
|
||||
self.endRemoveRows()
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def clear(self) -> None:
|
||||
# Reimplemented for performance reasons (begin/endRemoveRows)
|
||||
self.beginRemoveRows(QModelIndex(), 0, self.rowCount())
|
||||
self._list.clear()
|
||||
self.endRemoveRows()
|
||||
|
||||
|
||||
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()
|
31
harmonyqml/backend/model/qml_models.py
Normal file
31
harmonyqml/backend/model/qml_models.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
from typing import DefaultDict, Dict
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty
|
||||
|
||||
from .list_model import ListModel, _QtListModel
|
||||
|
||||
|
||||
class QMLModels(QObject):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._accounts: ListModel = ListModel()
|
||||
self._rooms: DefaultDict[str, ListModel] = DefaultDict(ListModel)
|
||||
self._messages: DefaultDict[str, ListModel] = DefaultDict(ListModel)
|
||||
|
||||
|
||||
@pyqtProperty(_QtListModel, constant=True)
|
||||
def accounts(self) -> _QtListModel:
|
||||
return self._accounts.qt_model
|
||||
|
||||
|
||||
@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("QVariantMap", constant=True)
|
||||
def messages(self) -> Dict[str, _QtListModel]:
|
||||
return {room_id: l.qt_model for room_id, l in self._messages.items()}
|
Reference in New Issue
Block a user