119 lines
3.5 KiB
Python
119 lines
3.5 KiB
Python
# 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
|
|
class ModelItem:
|
|
"""Base class for items stored inside a `Model`.
|
|
|
|
This class must be subclassed and not used directly.
|
|
All subclasses must be dataclasses.
|
|
|
|
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.
|
|
"""
|
|
|
|
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:
|
|
"""If this item is in a `Model`, alert it of attribute changes."""
|
|
|
|
if (
|
|
name == "parent_model" or
|
|
not self.parent_model or
|
|
getattr(self, name) == value
|
|
):
|
|
super().__setattr__(name, value)
|
|
return
|
|
|
|
super().__setattr__(name, value)
|
|
self._notify_parent_model({name: self.serialize_field(name)})
|
|
|
|
|
|
def _notify_parent_model(self, changed_fields: Dict[str, Any]) -> None:
|
|
parent = self.parent_model
|
|
|
|
if not parent or not parent.sync_id or not changed_fields:
|
|
return
|
|
|
|
with parent.write_lock:
|
|
index_then = parent._sorted_data.index(self)
|
|
parent._sorted_data.sort()
|
|
index_now = parent._sorted_data.index(self)
|
|
|
|
ModelItemSet(parent.sync_id, index_then, index_now, changed_fields)
|
|
|
|
for sync_id, proxy in parent.proxies.items():
|
|
if sync_id != parent.sync_id:
|
|
proxy.source_item_set(parent, self.id, self, changed_fields)
|
|
|
|
|
|
def __delattr__(self, name: str) -> None:
|
|
raise NotImplementedError()
|
|
|
|
|
|
def serialize_field(self, field: str) -> Any:
|
|
return serialize_value_for_qml(
|
|
getattr(self, field),
|
|
json_list_dicts=True,
|
|
)
|
|
|
|
|
|
@property
|
|
def serialized(self) -> Dict[str, Any]:
|
|
"""Return this item as a dict ready to be passed to QML."""
|
|
|
|
return {
|
|
name: self.serialize_field(name) for name in dir(self)
|
|
if not (
|
|
name.startswith("_") or name in ("parent_model", "serialized")
|
|
)
|
|
}
|
|
|
|
|
|
def set_fields(self, **fields: Any) -> None:
|
|
"""Set multiple fields's values at once.
|
|
|
|
The parent model will be resorted only once, and one `ModelItemSet`
|
|
event will be sent informing QML of all the changed fields.
|
|
"""
|
|
|
|
for name, value in fields.copy().items():
|
|
if getattr(self, name) == value:
|
|
del fields[name]
|
|
else:
|
|
super().__setattr__(name, value)
|
|
fields[name] = self.serialize_field(name)
|
|
|
|
self._notify_parent_model(fields)
|
|
|
|
|
|
def notify_change(self, *fields: str) -> None:
|
|
"""Manually notify the parent model that a field changed.
|
|
|
|
The model cannot automatically detect changes inside object
|
|
fields, such as list or dicts having their data modified.
|
|
|
|
Use this method to manually notify it that such fields were changed,
|
|
and avoid having to reassign the field itself.
|
|
"""
|
|
|
|
self._notify_parent_model({
|
|
name: self.serialize_field(name) for name in fields
|
|
})
|