pythonnative 0.20.0__py3-none-any.whl → 0.22.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythonnative/__init__.py +14 -3
- pythonnative/animated.py +420 -135
- pythonnative/cli/pn.py +450 -956
- pythonnative/components.py +519 -235
- pythonnative/events.py +210 -0
- pythonnative/gestures.py +875 -0
- pythonnative/layout.py +463 -149
- pythonnative/mutations.py +130 -0
- pythonnative/native_views/__init__.py +161 -97
- pythonnative/native_views/android.py +1050 -1124
- pythonnative/native_views/base.py +108 -18
- pythonnative/native_views/desktop.py +460 -417
- pythonnative/native_views/ios.py +1918 -1916
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +504 -0
- pythonnative/project/builder.py +555 -0
- pythonnative/project/config.py +642 -0
- pythonnative/project/doctor.py +233 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +344 -0
- pythonnative/project/permissions.py +343 -0
- pythonnative/project/runtime_assets.py +272 -0
- pythonnative/reconciler.py +540 -470
- pythonnative/screen.py +5 -2
- pythonnative/sdk/_components.py +2 -2
- pythonnative/templates/android_template/app/build.gradle +2 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/METADATA +10 -2
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/RECORD +32 -21
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -129
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Batched mutation protocol between the reconciler and native backends.
|
|
2
|
+
|
|
3
|
+
The reconciler no longer talks to the native layer one call at a time.
|
|
4
|
+
Instead, every commit pass produces an ordered list of small mutation
|
|
5
|
+
ops referencing integer **tags** (stable per-view identifiers), and the
|
|
6
|
+
whole list is applied in a single
|
|
7
|
+
[`apply_mutations`][pythonnative.native_views.NativeViewRegistry.apply_mutations]
|
|
8
|
+
call. This mirrors React Native's Fabric mounting layer: the diff phase
|
|
9
|
+
is pure, and the native side sees one coherent transaction per commit.
|
|
10
|
+
|
|
11
|
+
Why tags instead of view objects?
|
|
12
|
+
|
|
13
|
+
- The diff phase runs *before* any native view exists, so ops cannot
|
|
14
|
+
reference views directly.
|
|
15
|
+
- Tags give the native side a stable identity to key its own view
|
|
16
|
+
registry, event routing, and animation bookkeeping on.
|
|
17
|
+
- A flat list of `(op, tag, payload)` tuples is trivially serializable,
|
|
18
|
+
which keeps the door open for applying mutations from a background
|
|
19
|
+
thread or through a single JNI/ObjC crossing in the future.
|
|
20
|
+
|
|
21
|
+
Op ordering rules (the reconciler guarantees these):
|
|
22
|
+
|
|
23
|
+
1. A `CreateOp` for a tag precedes any other op referencing that tag.
|
|
24
|
+
2. `InsertOp` ops appear after both the parent and child exist.
|
|
25
|
+
3. `DestroyOp` ops appear after the corresponding `RemoveOp` (if the
|
|
26
|
+
node was attached) and are emitted children-first.
|
|
27
|
+
4. `SetFrameOp` ops are only emitted for frames that actually changed
|
|
28
|
+
since the last layout pass (frame diffing).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from typing import Any, Dict, Tuple, Union
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"CreateOp",
|
|
36
|
+
"UpdateOp",
|
|
37
|
+
"InsertOp",
|
|
38
|
+
"RemoveOp",
|
|
39
|
+
"DestroyOp",
|
|
40
|
+
"SetFrameOp",
|
|
41
|
+
"Mutation",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class CreateOp:
|
|
47
|
+
"""Create a native view for ``tag`` of element type ``type_name``.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
tag: Unique integer identity assigned by the reconciler.
|
|
51
|
+
type_name: Element type name (e.g. ``"Text"``).
|
|
52
|
+
props: Initial *clean* props — callables have already been
|
|
53
|
+
routed to the [`EventRegistry`][pythonnative.events.EventRegistry]
|
|
54
|
+
and replaced by the ``_pn_events`` name set.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
tag: int
|
|
58
|
+
type_name: str
|
|
59
|
+
props: Dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class UpdateOp:
|
|
64
|
+
"""Apply ``changed_props`` to the view registered under ``tag``.
|
|
65
|
+
|
|
66
|
+
Removed props are signaled with a value of ``None``, matching the
|
|
67
|
+
pre-existing handler contract.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
tag: int
|
|
71
|
+
changed_props: Dict[str, Any] = field(default_factory=dict)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class InsertOp:
|
|
76
|
+
"""Ensure the child view sits at ``index`` inside the parent view.
|
|
77
|
+
|
|
78
|
+
Handlers must treat this as *move-aware*: if the child is already
|
|
79
|
+
attached to the parent at a different position, it is moved rather
|
|
80
|
+
than duplicated. ``index`` is clamped by handlers to the current
|
|
81
|
+
child count.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
parent_tag: int
|
|
85
|
+
child_tag: int
|
|
86
|
+
index: int
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class RemoveOp:
|
|
91
|
+
"""Detach the child view from the parent view (without destroying it)."""
|
|
92
|
+
|
|
93
|
+
parent_tag: int
|
|
94
|
+
child_tag: int
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class DestroyOp:
|
|
99
|
+
"""Release the native view registered under ``tag``.
|
|
100
|
+
|
|
101
|
+
The registry drops its tag record and calls the handler's
|
|
102
|
+
``destroy`` hook so platform resources (listeners, timers, image
|
|
103
|
+
loads) can be released eagerly instead of waiting for GC.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
tag: int
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class SetFrameOp:
|
|
111
|
+
"""Position and size the view registered under ``tag``.
|
|
112
|
+
|
|
113
|
+
Coordinates are points relative to the parent's content origin,
|
|
114
|
+
exactly as computed by the layout engine.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
tag: int
|
|
118
|
+
x: float
|
|
119
|
+
y: float
|
|
120
|
+
width: float
|
|
121
|
+
height: float
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def frame(self) -> Tuple[float, float, float, float]:
|
|
125
|
+
"""Return ``(x, y, width, height)`` as a tuple."""
|
|
126
|
+
return (self.x, self.y, self.width, self.height)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
Mutation = Union[CreateOp, UpdateOp, InsertOp, RemoveOp, DestroyOp, SetFrameOp]
|
|
130
|
+
"""Union of every op type carried by a commit transaction."""
|
|
@@ -5,8 +5,15 @@ This package provides the
|
|
|
5
5
|
that maps element type names (e.g., `"Text"`, `"Button"`) to
|
|
6
6
|
platform-specific
|
|
7
7
|
[`ViewHandler`][pythonnative.native_views.base.ViewHandler]
|
|
8
|
-
implementations
|
|
9
|
-
|
|
8
|
+
implementations, and owns the **tag table** mapping each
|
|
9
|
+
reconciler-assigned integer tag to its live native view.
|
|
10
|
+
|
|
11
|
+
The reconciler communicates exclusively through
|
|
12
|
+
[`apply_mutations`][pythonnative.native_views.NativeViewRegistry.apply_mutations]:
|
|
13
|
+
one ordered batch of create/update/insert/remove/destroy/frame ops per
|
|
14
|
+
commit (see `pythonnative.mutations`). Imperative escape hatches
|
|
15
|
+
(commands, animation control, intrinsic measurement) resolve views
|
|
16
|
+
through the same tag table.
|
|
10
17
|
|
|
11
18
|
Platform handlers live in dedicated submodules:
|
|
12
19
|
|
|
@@ -15,6 +22,7 @@ Platform handlers live in dedicated submodules:
|
|
|
15
22
|
- `pythonnative.native_views.android`: Android handlers
|
|
16
23
|
(Chaquopy / Java bridge).
|
|
17
24
|
- `pythonnative.native_views.ios`: iOS handlers (rubicon-objc).
|
|
25
|
+
- `pythonnative.native_views.desktop`: Tkinter preview handlers.
|
|
18
26
|
|
|
19
27
|
All platform-branching is handled at registration time via lazy
|
|
20
28
|
imports, so this package can be imported on any platform for testing.
|
|
@@ -27,8 +35,9 @@ import math
|
|
|
27
35
|
import sys
|
|
28
36
|
import threading
|
|
29
37
|
import time
|
|
30
|
-
from typing import Any, Dict, Optional, Tuple
|
|
38
|
+
from typing import Any, Dict, Optional, Sequence, Tuple
|
|
31
39
|
|
|
40
|
+
from ..mutations import CreateOp, DestroyOp, InsertOp, Mutation, RemoveOp, SetFrameOp, UpdateOp
|
|
32
41
|
from .base import ViewHandler
|
|
33
42
|
|
|
34
43
|
# ======================================================================
|
|
@@ -85,17 +94,30 @@ def _tripwire_log(label: str, message: str) -> None:
|
|
|
85
94
|
pass
|
|
86
95
|
|
|
87
96
|
|
|
97
|
+
class ViewRecord:
|
|
98
|
+
"""One live native view tracked by the tag table."""
|
|
99
|
+
|
|
100
|
+
__slots__ = ("tag", "type_name", "view", "handler")
|
|
101
|
+
|
|
102
|
+
def __init__(self, tag: int, type_name: str, view: Any, handler: ViewHandler) -> None:
|
|
103
|
+
self.tag = tag
|
|
104
|
+
self.type_name = type_name
|
|
105
|
+
self.view = view
|
|
106
|
+
self.handler = handler
|
|
107
|
+
|
|
108
|
+
|
|
88
109
|
class NativeViewRegistry:
|
|
89
|
-
"""Map element type names to
|
|
110
|
+
"""Map element type names to handlers and tags to live native views.
|
|
90
111
|
|
|
91
|
-
The reconciler depends only on this protocol:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
The reconciler depends only on this protocol: ``apply_mutations``,
|
|
113
|
+
``resolve_view``, ``measure_intrinsic``, and ``command``.
|
|
114
|
+
Implementations may host real platform handlers (Android/iOS/
|
|
115
|
+
desktop) or mocks for tests.
|
|
95
116
|
"""
|
|
96
117
|
|
|
97
118
|
def __init__(self) -> None:
|
|
98
119
|
self._handlers: Dict[str, ViewHandler] = {}
|
|
120
|
+
self._records: Dict[int, ViewRecord] = {}
|
|
99
121
|
|
|
100
122
|
def register(self, type_name: str, handler: ViewHandler) -> None:
|
|
101
123
|
"""Register `handler` to service elements of type `type_name`.
|
|
@@ -106,116 +128,115 @@ class NativeViewRegistry:
|
|
|
106
128
|
"""
|
|
107
129
|
self._handlers[type_name] = handler
|
|
108
130
|
|
|
109
|
-
def
|
|
110
|
-
"""
|
|
131
|
+
def handler_for(self, type_name: str) -> Optional[ViewHandler]:
|
|
132
|
+
"""Return the handler registered for ``type_name``, if any."""
|
|
133
|
+
return self._handlers.get(type_name)
|
|
111
134
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
# Tag table
|
|
137
|
+
# ------------------------------------------------------------------
|
|
115
138
|
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
def resolve_view(self, tag: int) -> Any:
|
|
140
|
+
"""Return the native view registered under ``tag``, or ``None``."""
|
|
141
|
+
record = self._records.get(tag)
|
|
142
|
+
return record.view if record is not None else None
|
|
118
143
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
handler = self._handlers.get(type_name)
|
|
123
|
-
if handler is None:
|
|
124
|
-
raise ValueError(f"Unknown element type: {type_name!r}")
|
|
125
|
-
return handler.create(props)
|
|
144
|
+
def record_for(self, tag: int) -> Optional[ViewRecord]:
|
|
145
|
+
"""Return the full [`ViewRecord`][pythonnative.native_views.ViewRecord] for ``tag``."""
|
|
146
|
+
return self._records.get(tag)
|
|
126
147
|
|
|
127
|
-
def
|
|
128
|
-
"""
|
|
148
|
+
def live_view_count(self) -> int:
|
|
149
|
+
"""Number of views currently tracked (test/diagnostic helper)."""
|
|
150
|
+
return len(self._records)
|
|
129
151
|
|
|
130
|
-
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
# The commit channel
|
|
154
|
+
# ------------------------------------------------------------------
|
|
131
155
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
type_name: The element type name.
|
|
135
|
-
changed_props: A dict containing only props whose values
|
|
136
|
-
changed since the previous render. Removed props are
|
|
137
|
-
signaled with a value of `None`.
|
|
138
|
-
"""
|
|
139
|
-
handler = self._handlers.get(type_name)
|
|
140
|
-
if handler is not None:
|
|
141
|
-
handler.update(native_view, changed_props)
|
|
142
|
-
|
|
143
|
-
def add_child(self, parent: Any, child: Any, parent_type: str) -> None:
|
|
144
|
-
"""Append `child` to `parent`.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
parent: Parent native view.
|
|
148
|
-
child: Native view to append.
|
|
149
|
-
parent_type: Element type of the parent (for handler lookup).
|
|
150
|
-
"""
|
|
151
|
-
handler = self._handlers.get(parent_type)
|
|
152
|
-
if handler is not None:
|
|
153
|
-
handler.add_child(parent, child)
|
|
156
|
+
def apply_mutations(self, ops: Sequence[Mutation]) -> None:
|
|
157
|
+
"""Apply one commit transaction.
|
|
154
158
|
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
Ops are applied strictly in order. Failures are isolated per
|
|
160
|
+
op: a handler exception is logged (rate-limited) and the
|
|
161
|
+
remaining ops still apply, so one bad prop can't desync the
|
|
162
|
+
whole native tree.
|
|
157
163
|
|
|
158
164
|
Args:
|
|
159
|
-
|
|
160
|
-
child: Child native view to remove.
|
|
161
|
-
parent_type: Element type of the parent.
|
|
162
|
-
"""
|
|
163
|
-
handler = self._handlers.get(parent_type)
|
|
164
|
-
if handler is not None:
|
|
165
|
-
handler.remove_child(parent, child)
|
|
166
|
-
|
|
167
|
-
def insert_child(self, parent: Any, child: Any, parent_type: str, index: int) -> None:
|
|
168
|
-
"""Insert `child` into `parent` at `index`.
|
|
169
|
-
|
|
170
|
-
Args:
|
|
171
|
-
parent: Parent native view.
|
|
172
|
-
child: Child native view to insert.
|
|
173
|
-
parent_type: Element type of the parent.
|
|
174
|
-
index: Zero-based insertion position among `parent`'s
|
|
175
|
-
existing children.
|
|
176
|
-
"""
|
|
177
|
-
handler = self._handlers.get(parent_type)
|
|
178
|
-
if handler is not None:
|
|
179
|
-
handler.insert_child(parent, child, index)
|
|
180
|
-
|
|
181
|
-
def set_frame(
|
|
182
|
-
self,
|
|
183
|
-
native_view: Any,
|
|
184
|
-
type_name: str,
|
|
185
|
-
x: float,
|
|
186
|
-
y: float,
|
|
187
|
-
width: float,
|
|
188
|
-
height: float,
|
|
189
|
-
) -> None:
|
|
190
|
-
"""Position and size a native view via the appropriate handler.
|
|
191
|
-
|
|
192
|
-
Called by the reconciler's layout pass after every commit, with
|
|
193
|
-
coordinates computed by ``pythonnative.layout`` in points
|
|
194
|
-
relative to the parent's content origin.
|
|
165
|
+
ops: Ordered mutations emitted by the reconciler.
|
|
195
166
|
"""
|
|
167
|
+
for op in ops:
|
|
168
|
+
try:
|
|
169
|
+
self._apply_one(op)
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
_tripwire_log(
|
|
172
|
+
f"apply:{type(op).__name__}",
|
|
173
|
+
f"[PN] apply_mutations: {type(op).__name__} failed: {type(exc).__name__}: {exc!r}",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _apply_one(self, op: Mutation) -> None:
|
|
177
|
+
if isinstance(op, CreateOp):
|
|
178
|
+
handler = self._handlers.get(op.type_name)
|
|
179
|
+
if handler is None:
|
|
180
|
+
raise ValueError(f"Unknown element type: {op.type_name!r}")
|
|
181
|
+
view = handler.create(op.tag, op.props)
|
|
182
|
+
self._records[op.tag] = ViewRecord(op.tag, op.type_name, view, handler)
|
|
183
|
+
return
|
|
184
|
+
if isinstance(op, UpdateOp):
|
|
185
|
+
record = self._records.get(op.tag)
|
|
186
|
+
if record is not None:
|
|
187
|
+
record.handler.update(record.view, op.changed_props)
|
|
188
|
+
return
|
|
189
|
+
if isinstance(op, InsertOp):
|
|
190
|
+
parent = self._records.get(op.parent_tag)
|
|
191
|
+
child = self._records.get(op.child_tag)
|
|
192
|
+
if parent is not None and child is not None:
|
|
193
|
+
parent.handler.insert_child(parent.view, child.view, op.index)
|
|
194
|
+
return
|
|
195
|
+
if isinstance(op, RemoveOp):
|
|
196
|
+
parent = self._records.get(op.parent_tag)
|
|
197
|
+
child = self._records.get(op.child_tag)
|
|
198
|
+
if parent is not None and child is not None:
|
|
199
|
+
parent.handler.remove_child(parent.view, child.view)
|
|
200
|
+
return
|
|
201
|
+
if isinstance(op, DestroyOp):
|
|
202
|
+
record = self._records.pop(op.tag, None)
|
|
203
|
+
if record is not None:
|
|
204
|
+
record.handler.destroy(record.view)
|
|
205
|
+
return
|
|
206
|
+
if isinstance(op, SetFrameOp):
|
|
207
|
+
self._apply_frame(op)
|
|
208
|
+
return
|
|
209
|
+
raise TypeError(f"Unknown mutation op: {op!r}")
|
|
210
|
+
|
|
211
|
+
def _apply_frame(self, op: SetFrameOp) -> None:
|
|
212
|
+
record = self._records.get(op.tag)
|
|
213
|
+
if record is None:
|
|
214
|
+
return
|
|
196
215
|
# Tripwire: log non-finite layout values so we can diagnose
|
|
197
216
|
# crashes like iOS `CALayerInvalidGeometry` without losing the
|
|
198
217
|
# repro. Handlers are responsible for clamping before applying.
|
|
199
|
-
# Rate-limited via ``_tripwire_log`` to avoid
|
|
200
|
-
#
|
|
218
|
+
# Rate-limited via ``_tripwire_log`` to avoid floods when an
|
|
219
|
+
# animated value is stuck at NaN.
|
|
201
220
|
try:
|
|
202
|
-
finite =
|
|
221
|
+
finite = (
|
|
222
|
+
math.isfinite(op.x) and math.isfinite(op.y) and math.isfinite(op.width) and math.isfinite(op.height)
|
|
223
|
+
)
|
|
203
224
|
except (TypeError, ValueError):
|
|
204
225
|
finite = False
|
|
205
226
|
if not finite:
|
|
206
227
|
_tripwire_log(
|
|
207
228
|
"set_frame:nan",
|
|
208
|
-
f"[set_frame:nan] type={type_name!r} " f"x={x!r} y={y!r} w={width!r} h={height!r}",
|
|
229
|
+
f"[set_frame:nan] type={record.type_name!r} " f"x={op.x!r} y={op.y!r} w={op.width!r} h={op.height!r}",
|
|
209
230
|
)
|
|
231
|
+
record.handler.set_frame(record.view, op.x, op.y, op.width, op.height)
|
|
210
232
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
# Imperative escape hatches (resolved through the tag table)
|
|
235
|
+
# ------------------------------------------------------------------
|
|
214
236
|
|
|
215
237
|
def measure_intrinsic(
|
|
216
238
|
self,
|
|
217
|
-
|
|
218
|
-
type_name: str,
|
|
239
|
+
tag: int,
|
|
219
240
|
max_width: float,
|
|
220
241
|
max_height: float,
|
|
221
242
|
) -> Tuple[float, float]:
|
|
@@ -224,10 +245,53 @@ class NativeViewRegistry:
|
|
|
224
245
|
Used by the layout engine for leaves whose intrinsic size
|
|
225
246
|
depends on their content (text, buttons, images).
|
|
226
247
|
"""
|
|
227
|
-
|
|
228
|
-
if
|
|
248
|
+
record = self._records.get(tag)
|
|
249
|
+
if record is None:
|
|
229
250
|
return (0.0, 0.0)
|
|
230
|
-
return handler.measure_intrinsic(
|
|
251
|
+
return record.handler.measure_intrinsic(record.view, max_width, max_height)
|
|
252
|
+
|
|
253
|
+
def command(self, tag: int, name: str, args: Optional[Dict[str, Any]] = None) -> Any:
|
|
254
|
+
"""Execute an imperative command against the view for ``tag``.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
tag: Target view tag.
|
|
258
|
+
name: Command name (handler-specific, e.g.
|
|
259
|
+
``"scroll_to_offset"``).
|
|
260
|
+
args: Optional command arguments.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
The handler's command result, or ``None`` when the tag is
|
|
264
|
+
unknown.
|
|
265
|
+
"""
|
|
266
|
+
record = self._records.get(tag)
|
|
267
|
+
if record is None:
|
|
268
|
+
return None
|
|
269
|
+
return record.handler.command(record.view, name, args or {})
|
|
270
|
+
|
|
271
|
+
def set_animated_property(self, tag: int, prop_name: str, value: Any) -> None:
|
|
272
|
+
"""Apply one Python-driven animation frame to the view for ``tag``."""
|
|
273
|
+
record = self._records.get(tag)
|
|
274
|
+
if record is not None:
|
|
275
|
+
record.handler.set_animated_property(record.view, prop_name, value)
|
|
276
|
+
|
|
277
|
+
def start_animation(self, tag: int, anim_id: int, prop_name: str, spec: Dict[str, Any]) -> bool:
|
|
278
|
+
"""Start a natively-driven animation on the view for ``tag``.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Whether the platform accepted the animation (``False``
|
|
282
|
+
means the caller should drive it from the Python ticker).
|
|
283
|
+
"""
|
|
284
|
+
record = self._records.get(tag)
|
|
285
|
+
if record is None:
|
|
286
|
+
return False
|
|
287
|
+
return bool(record.handler.start_animation(record.view, anim_id, prop_name, spec))
|
|
288
|
+
|
|
289
|
+
def cancel_animation(self, tag: int, anim_id: int) -> Any:
|
|
290
|
+
"""Cancel a natively-driven animation; returns the presentation value if known."""
|
|
291
|
+
record = self._records.get(tag)
|
|
292
|
+
if record is None:
|
|
293
|
+
return None
|
|
294
|
+
return record.handler.cancel_animation(record.view, anim_id)
|
|
231
295
|
|
|
232
296
|
|
|
233
297
|
# ======================================================================
|