130 lines
4.2 KiB
Python
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)
|