moment/src/backend/models/model_item.py

130 lines
4.2 KiB
Python

# Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
# SPDX-License-Identifier: LGPL-3.0-or-later
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Dict, Optional
from ..pyotherside_events import ModelItemSet
from ..utils import serialize_value_for_qml
if TYPE_CHECKING:
from .model import Model
@dataclass(eq=False)
class ModelItem:
"""Base class for items stored inside a `Model`.
This class must be subclassed and not used directly.
All subclasses must use the `@dataclass(eq=False)` decorator.
Subclasses are also expected to implement `__lt__()`,
to provide support for comparisons with the `<`, `>`, `<=`, `=>` operators
and thus allow a `Model` to keep its data sorted.
Make sure to respect SortedList requirements when implementing `__lt__()`:
http://www.grantjenks.com/docs/sortedcontainers/introduction.html#caveats
"""
id: Any = field()
def __new__(cls, *_args, **_kwargs) -> "ModelItem":
cls.parent_model: Optional[Model] = None
return super().__new__(cls)
def __setattr__(self, name: str, value) -> None:
self.set_fields(**{name: value})
def __delattr__(self, name: str) -> None:
raise NotImplementedError()
@property
def serialized(self) -> Dict[str, Any]:
"""Return this item as a dict ready to be passed to QML."""
return {
name: self.serialized_field(name)
for name in self.__dataclass_fields__ # type: ignore
if not name.startswith("_")
}
def serialized_field(self, field: str) -> Any:
"""Return a field's value in a form suitable for passing to QML."""
value = getattr(self, field)
return serialize_value_for_qml(value, json_list_dicts=True)
def set_fields(self, _force: bool = False, **fields: Any) -> None:
"""Set one or more field's value and call `ModelItem.notify_change`.
For efficiency, to change multiple fields, this method should be
used rather than setting them one after another with `=` or `setattr`.
"""
parent = self.parent_model
# If we're currently being created or haven't been put in a model yet:
if not parent:
for name, value in fields.items():
super().__setattr__(name, value)
return
with parent.write_lock:
qml_changes = {}
changes = {
name: value for name, value in fields.items()
if _force or getattr(self, name) != value
}
if not changes:
return
# To avoid corrupting the SortedList, we have to take out the item,
# apply the field changes, *then* add it back in.
index_then = parent._sorted_data.index(self)
del parent._sorted_data[index_then]
for name, value in changes.items():
super().__setattr__(name, value)
is_field = name in self.__dataclass_fields__ # type: ignore
if is_field and not name.startswith("_"):
qml_changes[name] = self.serialized_field(name)
parent._sorted_data.add(self)
index_now = parent._sorted_data.index(self)
index_change = index_then != index_now
# Now, inform QML about changed dataclass fields if any.
if not parent.sync_id or (not qml_changes and not index_change):
return
ModelItemSet(parent.sync_id, index_then, index_now, qml_changes)
# Inform any proxy connected to the parent model of the field changes
for sync_id, proxy in parent.proxies.items():
if sync_id != parent.sync_id:
proxy.source_item_set(parent, self.id, self, qml_changes)
def notify_change(self, *fields: str) -> None:
"""Notify the parent model that fields of this item have changed.
The model cannot automatically detect changes inside
object fields, such as list or dicts having their data modified.
In these cases, this method should be called.
"""
kwargs = {name: getattr(self, name) for name in fields}
kwargs["_force"] = True
self.set_fields(**kwargs)