pythonnative 0.3.0__py3-none-any.whl → 0.5.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 +45 -65
- pythonnative/cli/pn.py +16 -10
- pythonnative/components.py +241 -0
- pythonnative/element.py +47 -0
- pythonnative/native_views.py +800 -0
- pythonnative/page.py +321 -249
- pythonnative/reconciler.py +129 -0
- pythonnative/templates/android_template/app/build.gradle +2 -2
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -1
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
- pythonnative/templates/android_template/build.gradle +3 -3
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +1 -1
- pythonnative/utils.py +21 -29
- pythonnative-0.5.0.dist-info/METADATA +161 -0
- {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/RECORD +19 -39
- {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/licenses/LICENSE +1 -1
- pythonnative/activity_indicator_view.py +0 -71
- pythonnative/button.py +0 -109
- pythonnative/date_picker.py +0 -72
- pythonnative/image_view.py +0 -76
- pythonnative/label.py +0 -66
- pythonnative/list_view.py +0 -73
- pythonnative/material_activity_indicator_view.py +0 -69
- pythonnative/material_button.py +0 -65
- pythonnative/material_date_picker.py +0 -85
- pythonnative/material_progress_view.py +0 -66
- pythonnative/material_search_bar.py +0 -65
- pythonnative/material_switch.py +0 -65
- pythonnative/material_time_picker.py +0 -72
- pythonnative/picker_view.py +0 -65
- pythonnative/progress_view.py +0 -68
- pythonnative/scroll_view.py +0 -63
- pythonnative/search_bar.py +0 -65
- pythonnative/stack_view.py +0 -60
- pythonnative/switch.py +0 -66
- pythonnative/text_field.py +0 -67
- pythonnative/text_view.py +0 -70
- pythonnative/time_picker.py +0 -73
- pythonnative/view.py +0 -25
- pythonnative/web_view.py +0 -58
- pythonnative-0.3.0.dist-info/METADATA +0 -137
- {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/top_level.txt +0 -0
pythonnative/page.py
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Just ensure that your PythonNative UI framework is aware of these platform
|
|
29
|
-
differences and handles them appropriately.
|
|
1
|
+
"""Page — the root component that bridges native lifecycle and declarative UI.
|
|
2
|
+
|
|
3
|
+
A ``Page`` subclass is the entry point for each screen. It owns a
|
|
4
|
+
:class:`~pythonnative.reconciler.Reconciler` and automatically mounts /
|
|
5
|
+
re-renders the element tree returned by :meth:`render` whenever state
|
|
6
|
+
changes.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
import pythonnative as pn
|
|
11
|
+
|
|
12
|
+
class MainPage(pn.Page):
|
|
13
|
+
def __init__(self, native_instance):
|
|
14
|
+
super().__init__(native_instance)
|
|
15
|
+
self.state = {"count": 0}
|
|
16
|
+
|
|
17
|
+
def increment(self):
|
|
18
|
+
self.set_state(count=self.state["count"] + 1)
|
|
19
|
+
|
|
20
|
+
def render(self):
|
|
21
|
+
return pn.Column(
|
|
22
|
+
pn.Text(f"Count: {self.state['count']}", font_size=24),
|
|
23
|
+
pn.Button("Increment", on_click=self.increment),
|
|
24
|
+
spacing=12,
|
|
25
|
+
padding=16,
|
|
26
|
+
)
|
|
30
27
|
"""
|
|
31
28
|
|
|
32
29
|
import json
|
|
@@ -34,55 +31,50 @@ from abc import ABC, abstractmethod
|
|
|
34
31
|
from typing import Any, Optional, Union
|
|
35
32
|
|
|
36
33
|
from .utils import IS_ANDROID, set_android_context
|
|
37
|
-
from .view import ViewBase
|
|
38
34
|
|
|
39
|
-
#
|
|
40
|
-
# Base class
|
|
41
|
-
#
|
|
35
|
+
# ======================================================================
|
|
36
|
+
# Base class (platform-independent)
|
|
37
|
+
# ======================================================================
|
|
42
38
|
|
|
43
39
|
|
|
44
40
|
class PageBase(ABC):
|
|
41
|
+
"""Abstract base defining the Page interface."""
|
|
42
|
+
|
|
45
43
|
@abstractmethod
|
|
46
44
|
def __init__(self) -> None:
|
|
47
45
|
super().__init__()
|
|
48
46
|
|
|
49
47
|
@abstractmethod
|
|
50
|
-
def
|
|
51
|
-
|
|
48
|
+
def render(self) -> Any:
|
|
49
|
+
"""Return an Element tree describing this page's UI."""
|
|
50
|
+
|
|
51
|
+
def set_state(self, **updates: Any) -> None:
|
|
52
|
+
"""Merge *updates* into ``self.state`` and trigger a re-render."""
|
|
52
53
|
|
|
53
|
-
@abstractmethod
|
|
54
54
|
def on_create(self) -> None:
|
|
55
|
-
|
|
55
|
+
"""Called when the page is first created. Triggers initial render."""
|
|
56
56
|
|
|
57
|
-
@abstractmethod
|
|
58
57
|
def on_start(self) -> None:
|
|
59
58
|
pass
|
|
60
59
|
|
|
61
|
-
@abstractmethod
|
|
62
60
|
def on_resume(self) -> None:
|
|
63
61
|
pass
|
|
64
62
|
|
|
65
|
-
@abstractmethod
|
|
66
63
|
def on_pause(self) -> None:
|
|
67
64
|
pass
|
|
68
65
|
|
|
69
|
-
@abstractmethod
|
|
70
66
|
def on_stop(self) -> None:
|
|
71
67
|
pass
|
|
72
68
|
|
|
73
|
-
@abstractmethod
|
|
74
69
|
def on_destroy(self) -> None:
|
|
75
70
|
pass
|
|
76
71
|
|
|
77
|
-
@abstractmethod
|
|
78
72
|
def on_restart(self) -> None:
|
|
79
73
|
pass
|
|
80
74
|
|
|
81
|
-
@abstractmethod
|
|
82
75
|
def on_save_instance_state(self) -> None:
|
|
83
76
|
pass
|
|
84
77
|
|
|
85
|
-
@abstractmethod
|
|
86
78
|
def on_restore_instance_state(self) -> None:
|
|
87
79
|
pass
|
|
88
80
|
|
|
@@ -99,141 +91,182 @@ class PageBase(ABC):
|
|
|
99
91
|
pass
|
|
100
92
|
|
|
101
93
|
def get_args(self) -> dict:
|
|
102
|
-
"""Return arguments
|
|
103
|
-
# Concrete classes should set self._args; default empty
|
|
94
|
+
"""Return navigation arguments (empty dict if none)."""
|
|
104
95
|
return getattr(self, "_args", {})
|
|
105
96
|
|
|
106
|
-
|
|
107
|
-
def navigate_to(self, page) -> None:
|
|
97
|
+
def navigate_to(self, page: Any) -> None:
|
|
108
98
|
self.push(page)
|
|
109
|
-
pass
|
|
110
99
|
|
|
111
100
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
101
|
+
# ======================================================================
|
|
102
|
+
# Shared declarative rendering helpers
|
|
103
|
+
# ======================================================================
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _init_page_common(page: Any) -> None:
|
|
107
|
+
"""Common initialisation shared by both platform Page classes."""
|
|
108
|
+
page.state = {}
|
|
109
|
+
page._args = {}
|
|
110
|
+
page._reconciler = None
|
|
111
|
+
page._root_native_view = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _set_state(page: Any, **updates: Any) -> None:
|
|
115
|
+
page.state.update(updates)
|
|
116
|
+
if page._reconciler is not None:
|
|
117
|
+
_re_render(page)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _on_create(page: Any) -> None:
|
|
121
|
+
from .native_views import get_registry
|
|
122
|
+
from .reconciler import Reconciler
|
|
123
|
+
|
|
124
|
+
page._reconciler = Reconciler(get_registry())
|
|
125
|
+
element = page.render()
|
|
126
|
+
page._root_native_view = page._reconciler.mount(element)
|
|
127
|
+
page._attach_root(page._root_native_view)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _re_render(page: Any) -> None:
|
|
131
|
+
element = page.render()
|
|
132
|
+
new_root = page._reconciler.reconcile(element)
|
|
133
|
+
if new_root is not page._root_native_view:
|
|
134
|
+
page._detach_root(page._root_native_view)
|
|
135
|
+
page._root_native_view = new_root
|
|
136
|
+
page._attach_root(new_root)
|
|
137
|
+
|
|
117
138
|
|
|
139
|
+
def _resolve_page_path(page_ref: Union[str, Any]) -> str:
|
|
140
|
+
if isinstance(page_ref, str):
|
|
141
|
+
return page_ref
|
|
142
|
+
module = getattr(page_ref, "__module__", None)
|
|
143
|
+
name = getattr(page_ref, "__name__", None)
|
|
144
|
+
if module and name:
|
|
145
|
+
return f"{module}.{name}"
|
|
146
|
+
cls = page_ref.__class__
|
|
147
|
+
return f"{cls.__module__}.{cls.__name__}"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _set_args(page: Any, args: Optional[dict]) -> None:
|
|
151
|
+
if isinstance(args, str):
|
|
152
|
+
try:
|
|
153
|
+
page._args = json.loads(args) or {}
|
|
154
|
+
except Exception:
|
|
155
|
+
page._args = {}
|
|
156
|
+
return
|
|
157
|
+
page._args = args or {}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ======================================================================
|
|
161
|
+
# Platform implementations
|
|
162
|
+
# ======================================================================
|
|
163
|
+
|
|
164
|
+
if IS_ANDROID:
|
|
118
165
|
from java import jclass
|
|
119
166
|
|
|
120
|
-
class Page(PageBase
|
|
121
|
-
|
|
167
|
+
class Page(PageBase):
|
|
168
|
+
"""Android Page backed by an Activity and Fragment navigation."""
|
|
169
|
+
|
|
170
|
+
def __init__(self, native_instance: Any) -> None:
|
|
122
171
|
super().__init__()
|
|
123
172
|
self.native_class = jclass("android.app.Activity")
|
|
124
173
|
self.native_instance = native_instance
|
|
125
|
-
# self.native_instance = self.native_class()
|
|
126
|
-
# Stash the Activity so child views can implicitly acquire a Context
|
|
127
174
|
set_android_context(native_instance)
|
|
128
|
-
self
|
|
175
|
+
_init_page_common(self)
|
|
129
176
|
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
try:
|
|
133
|
-
from .utils import get_android_fragment_container
|
|
177
|
+
def render(self) -> Any:
|
|
178
|
+
raise NotImplementedError("Page subclass must implement render()")
|
|
134
179
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
try:
|
|
138
|
-
container.removeAllViews()
|
|
139
|
-
except Exception:
|
|
140
|
-
pass
|
|
141
|
-
container.addView(view.native_instance)
|
|
142
|
-
except Exception:
|
|
143
|
-
# Fallback to setting content view directly on the Activity
|
|
144
|
-
self.native_instance.setContentView(view.native_instance)
|
|
180
|
+
def set_state(self, **updates: Any) -> None:
|
|
181
|
+
_set_state(self, **updates)
|
|
145
182
|
|
|
146
183
|
def on_create(self) -> None:
|
|
147
|
-
|
|
184
|
+
_on_create(self)
|
|
148
185
|
|
|
149
186
|
def on_start(self) -> None:
|
|
150
|
-
|
|
187
|
+
pass
|
|
151
188
|
|
|
152
189
|
def on_resume(self) -> None:
|
|
153
|
-
|
|
190
|
+
pass
|
|
154
191
|
|
|
155
192
|
def on_pause(self) -> None:
|
|
156
|
-
|
|
193
|
+
pass
|
|
157
194
|
|
|
158
195
|
def on_stop(self) -> None:
|
|
159
|
-
|
|
196
|
+
pass
|
|
160
197
|
|
|
161
198
|
def on_destroy(self) -> None:
|
|
162
|
-
|
|
199
|
+
pass
|
|
163
200
|
|
|
164
201
|
def on_restart(self) -> None:
|
|
165
|
-
|
|
202
|
+
pass
|
|
166
203
|
|
|
167
204
|
def on_save_instance_state(self) -> None:
|
|
168
|
-
|
|
205
|
+
pass
|
|
169
206
|
|
|
170
207
|
def on_restore_instance_state(self) -> None:
|
|
171
|
-
|
|
208
|
+
pass
|
|
172
209
|
|
|
173
210
|
def set_args(self, args: Optional[dict]) -> None:
|
|
174
|
-
|
|
175
|
-
if isinstance(args, str):
|
|
176
|
-
try:
|
|
177
|
-
self._args = json.loads(args) or {}
|
|
178
|
-
return
|
|
179
|
-
except Exception:
|
|
180
|
-
self._args = {}
|
|
181
|
-
return
|
|
182
|
-
self._args = args or {}
|
|
183
|
-
|
|
184
|
-
def _resolve_page_path(self, page: Union[str, Any]) -> str:
|
|
185
|
-
if isinstance(page, str):
|
|
186
|
-
return page
|
|
187
|
-
# If a class or instance is passed, derive dotted path
|
|
188
|
-
try:
|
|
189
|
-
module = getattr(page, "__module__", None)
|
|
190
|
-
name = getattr(page, "__name__", None)
|
|
191
|
-
if module and name:
|
|
192
|
-
return f"{module}.{name}"
|
|
193
|
-
# Instance: use its class
|
|
194
|
-
cls = page.__class__
|
|
195
|
-
return f"{cls.__module__}.{cls.__name__}"
|
|
196
|
-
except Exception:
|
|
197
|
-
raise ValueError("Unsupported page reference; expected dotted string or class/instance")
|
|
211
|
+
_set_args(self, args)
|
|
198
212
|
|
|
199
213
|
def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
args_json = json.dumps(args) if args else None
|
|
205
|
-
Navigator.push(self.native_instance, page_path, args_json)
|
|
206
|
-
except Exception:
|
|
207
|
-
# As a last resort, do nothing rather than crash
|
|
208
|
-
pass
|
|
214
|
+
page_path = _resolve_page_path(page)
|
|
215
|
+
Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
216
|
+
args_json = json.dumps(args) if args else None
|
|
217
|
+
Navigator.push(self.native_instance, page_path, args_json)
|
|
209
218
|
|
|
210
219
|
def pop(self) -> None:
|
|
211
|
-
# Delegate to Navigator.pop for back-stack pop
|
|
212
220
|
try:
|
|
213
221
|
Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
214
222
|
Navigator.pop(self.native_instance)
|
|
215
223
|
except Exception:
|
|
224
|
+
self.native_instance.finish()
|
|
225
|
+
|
|
226
|
+
def _attach_root(self, native_view: Any) -> None:
|
|
227
|
+
try:
|
|
228
|
+
from .utils import get_android_fragment_container
|
|
229
|
+
|
|
230
|
+
container = get_android_fragment_container()
|
|
216
231
|
try:
|
|
217
|
-
|
|
232
|
+
container.removeAllViews()
|
|
218
233
|
except Exception:
|
|
219
234
|
pass
|
|
235
|
+
LayoutParams = jclass("android.view.ViewGroup$LayoutParams")
|
|
236
|
+
lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
237
|
+
container.addView(native_view, lp)
|
|
238
|
+
except Exception:
|
|
239
|
+
self.native_instance.setContentView(native_view)
|
|
240
|
+
|
|
241
|
+
def _detach_root(self, native_view: Any) -> None:
|
|
242
|
+
try:
|
|
243
|
+
from .utils import get_android_fragment_container
|
|
244
|
+
|
|
245
|
+
container = get_android_fragment_container()
|
|
246
|
+
container.removeAllViews()
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
220
249
|
|
|
221
250
|
else:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
251
|
+
from typing import Dict as _Dict
|
|
252
|
+
|
|
253
|
+
_rubicon_available = False
|
|
254
|
+
try:
|
|
255
|
+
from rubicon.objc import ObjCClass, ObjCInstance
|
|
226
256
|
|
|
227
|
-
|
|
257
|
+
_rubicon_available = True
|
|
228
258
|
|
|
229
|
-
|
|
259
|
+
import gc as _gc
|
|
230
260
|
|
|
231
|
-
|
|
232
|
-
|
|
261
|
+
_gc.disable()
|
|
262
|
+
except ImportError:
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
_IOS_PAGE_REGISTRY: _Dict[int, Any] = {}
|
|
233
266
|
|
|
234
267
|
def _ios_register_page(vc_instance: Any, page_obj: Any) -> None:
|
|
235
268
|
try:
|
|
236
|
-
ptr = int(vc_instance.ptr)
|
|
269
|
+
ptr = int(vc_instance.ptr)
|
|
237
270
|
_IOS_PAGE_REGISTRY[ptr] = page_obj
|
|
238
271
|
except Exception:
|
|
239
272
|
pass
|
|
@@ -246,151 +279,190 @@ else:
|
|
|
246
279
|
pass
|
|
247
280
|
|
|
248
281
|
def forward_lifecycle(native_addr: int, event: str) -> None:
|
|
249
|
-
"""Forward a lifecycle event from Swift ViewController to the registered Page.
|
|
250
|
-
|
|
251
|
-
:param native_addr: Integer pointer address of the UIViewController
|
|
252
|
-
:param event: One of 'on_start', 'on_resume', 'on_pause', 'on_stop', 'on_destroy',
|
|
253
|
-
'on_save_instance_state', 'on_restore_instance_state'.
|
|
254
|
-
"""
|
|
282
|
+
"""Forward a lifecycle event from Swift ViewController to the registered Page."""
|
|
255
283
|
page = _IOS_PAGE_REGISTRY.get(int(native_addr))
|
|
256
|
-
if
|
|
284
|
+
if page is None:
|
|
257
285
|
return
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
handler()
|
|
262
|
-
except Exception:
|
|
263
|
-
# Avoid surfacing exceptions across the Swift/Python boundary in lifecycle
|
|
264
|
-
pass
|
|
286
|
+
handler = getattr(page, event, None)
|
|
287
|
+
if handler:
|
|
288
|
+
handler()
|
|
265
289
|
|
|
266
|
-
|
|
267
|
-
def __init__(self, native_instance) -> None:
|
|
268
|
-
super().__init__()
|
|
269
|
-
self.native_class = ObjCClass("UIViewController")
|
|
270
|
-
# If Swift passed us an integer pointer, wrap it as an ObjCInstance.
|
|
271
|
-
if isinstance(native_instance, int):
|
|
272
|
-
try:
|
|
273
|
-
native_instance = ObjCInstance(native_instance)
|
|
274
|
-
except Exception:
|
|
275
|
-
native_instance = None
|
|
276
|
-
self.native_instance = native_instance
|
|
277
|
-
# self.native_instance = self.native_class.alloc().init()
|
|
278
|
-
self._args: dict = {}
|
|
279
|
-
# Register for lifecycle forwarding
|
|
280
|
-
if self.native_instance is not None:
|
|
281
|
-
_ios_register_page(self.native_instance, self)
|
|
282
|
-
|
|
283
|
-
def set_root_view(self, view) -> None:
|
|
284
|
-
# UIViewController.view is a property; access without calling.
|
|
285
|
-
root_view = self.native_instance.view
|
|
286
|
-
# Size the root child to fill the controller's view and enable autoresizing
|
|
287
|
-
try:
|
|
288
|
-
bounds = root_view.bounds
|
|
289
|
-
view.native_instance.setFrame_(bounds)
|
|
290
|
-
# UIViewAutoresizingFlexibleWidth (2) | UIViewAutoresizingFlexibleHeight (16)
|
|
291
|
-
view.native_instance.setAutoresizingMask_(2 | 16)
|
|
292
|
-
except Exception:
|
|
293
|
-
pass
|
|
294
|
-
root_view.addSubview_(view.native_instance)
|
|
290
|
+
if _rubicon_available:
|
|
295
291
|
|
|
296
|
-
|
|
297
|
-
|
|
292
|
+
class Page(PageBase):
|
|
293
|
+
"""iOS Page backed by a UIViewController."""
|
|
298
294
|
|
|
299
|
-
|
|
300
|
-
|
|
295
|
+
def __init__(self, native_instance: Any) -> None:
|
|
296
|
+
super().__init__()
|
|
297
|
+
self.native_class = ObjCClass("UIViewController")
|
|
298
|
+
if isinstance(native_instance, int):
|
|
299
|
+
try:
|
|
300
|
+
native_instance = ObjCInstance(native_instance)
|
|
301
|
+
except Exception:
|
|
302
|
+
native_instance = None
|
|
303
|
+
self.native_instance = native_instance
|
|
304
|
+
_init_page_common(self)
|
|
305
|
+
if self.native_instance is not None:
|
|
306
|
+
_ios_register_page(self.native_instance, self)
|
|
301
307
|
|
|
302
|
-
|
|
303
|
-
|
|
308
|
+
def render(self) -> Any:
|
|
309
|
+
raise NotImplementedError("Page subclass must implement render()")
|
|
304
310
|
|
|
305
|
-
|
|
306
|
-
|
|
311
|
+
def set_state(self, **updates: Any) -> None:
|
|
312
|
+
_set_state(self, **updates)
|
|
307
313
|
|
|
308
|
-
|
|
309
|
-
|
|
314
|
+
def on_create(self) -> None:
|
|
315
|
+
_on_create(self)
|
|
310
316
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if self.native_instance is not None:
|
|
314
|
-
_ios_unregister_page(self.native_instance)
|
|
317
|
+
def on_start(self) -> None:
|
|
318
|
+
pass
|
|
315
319
|
|
|
316
|
-
|
|
317
|
-
|
|
320
|
+
def on_resume(self) -> None:
|
|
321
|
+
pass
|
|
318
322
|
|
|
319
|
-
|
|
320
|
-
|
|
323
|
+
def on_pause(self) -> None:
|
|
324
|
+
pass
|
|
321
325
|
|
|
322
|
-
|
|
323
|
-
|
|
326
|
+
def on_stop(self) -> None:
|
|
327
|
+
pass
|
|
324
328
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
self._args = json.loads(args) or {}
|
|
329
|
-
return
|
|
330
|
-
except Exception:
|
|
331
|
-
self._args = {}
|
|
332
|
-
return
|
|
333
|
-
self._args = args or {}
|
|
329
|
+
def on_destroy(self) -> None:
|
|
330
|
+
if self.native_instance is not None:
|
|
331
|
+
_ios_unregister_page(self.native_instance)
|
|
334
332
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return page
|
|
338
|
-
try:
|
|
339
|
-
module = getattr(page, "__module__", None)
|
|
340
|
-
name = getattr(page, "__name__", None)
|
|
341
|
-
if module and name:
|
|
342
|
-
return f"{module}.{name}"
|
|
343
|
-
cls = page.__class__
|
|
344
|
-
return f"{cls.__module__}.{cls.__name__}"
|
|
345
|
-
except Exception:
|
|
346
|
-
raise ValueError("Unsupported page reference; expected dotted string or class/instance")
|
|
333
|
+
def on_restart(self) -> None:
|
|
334
|
+
pass
|
|
347
335
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
336
|
+
def on_save_instance_state(self) -> None:
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
def on_restore_instance_state(self) -> None:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
def set_args(self, args: Optional[dict]) -> None:
|
|
343
|
+
_set_args(self, args)
|
|
344
|
+
|
|
345
|
+
def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
|
|
346
|
+
page_path = _resolve_page_path(page)
|
|
347
|
+
ViewController = None
|
|
356
348
|
try:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
module_name = None
|
|
349
|
+
ViewController = ObjCClass("ViewController")
|
|
350
|
+
except Exception:
|
|
360
351
|
try:
|
|
361
|
-
|
|
352
|
+
NSBundle = ObjCClass("NSBundle")
|
|
353
|
+
bundle = NSBundle.mainBundle
|
|
362
354
|
module_name = bundle.objectForInfoDictionaryKey_("CFBundleName")
|
|
363
355
|
if module_name is None:
|
|
364
356
|
module_name = bundle.objectForInfoDictionaryKey_("CFBundleExecutable")
|
|
357
|
+
if module_name:
|
|
358
|
+
ViewController = ObjCClass(f"{module_name}.ViewController")
|
|
365
359
|
except Exception:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
if ViewController is None:
|
|
363
|
+
raise NameError("ViewController class not found; ensure Swift class is ObjC-visible")
|
|
364
|
+
|
|
365
|
+
next_vc = ViewController.alloc().init()
|
|
366
|
+
try:
|
|
367
|
+
next_vc.setValue_forKey_(page_path, "requestedPagePath")
|
|
368
|
+
if args:
|
|
369
|
+
next_vc.setValue_forKey_(json.dumps(args), "requestedPageArgsJSON")
|
|
370
|
+
except Exception:
|
|
371
|
+
pass
|
|
372
|
+
nav = getattr(self.native_instance, "navigationController", None)
|
|
373
|
+
if nav is None:
|
|
374
|
+
raise RuntimeError(
|
|
375
|
+
"No UINavigationController available; ensure template embeds root in navigation controller"
|
|
376
|
+
)
|
|
377
|
+
nav.pushViewController_animated_(next_vc, True)
|
|
378
|
+
|
|
379
|
+
def pop(self) -> None:
|
|
380
|
+
nav = getattr(self.native_instance, "navigationController", None)
|
|
381
|
+
if nav is not None:
|
|
382
|
+
nav.popViewControllerAnimated_(True)
|
|
383
|
+
|
|
384
|
+
def _attach_root(self, native_view: Any) -> None:
|
|
385
|
+
root_view = self.native_instance.view
|
|
386
|
+
native_view.setTranslatesAutoresizingMaskIntoConstraints_(False)
|
|
387
|
+
root_view.addSubview_(native_view)
|
|
388
|
+
try:
|
|
389
|
+
safe = root_view.safeAreaLayoutGuide
|
|
390
|
+
native_view.topAnchor.constraintEqualToAnchor_(safe.topAnchor).setActive_(True)
|
|
391
|
+
native_view.bottomAnchor.constraintEqualToAnchor_(safe.bottomAnchor).setActive_(True)
|
|
392
|
+
native_view.leadingAnchor.constraintEqualToAnchor_(safe.leadingAnchor).setActive_(True)
|
|
393
|
+
native_view.trailingAnchor.constraintEqualToAnchor_(safe.trailingAnchor).setActive_(True)
|
|
369
394
|
except Exception:
|
|
370
|
-
|
|
395
|
+
native_view.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
396
|
+
try:
|
|
397
|
+
native_view.setFrame_(root_view.bounds)
|
|
398
|
+
native_view.setAutoresizingMask_(2 | 16)
|
|
399
|
+
except Exception:
|
|
400
|
+
pass
|
|
371
401
|
|
|
372
|
-
|
|
373
|
-
|
|
402
|
+
def _detach_root(self, native_view: Any) -> None:
|
|
403
|
+
try:
|
|
404
|
+
native_view.removeFromSuperview()
|
|
405
|
+
except Exception:
|
|
406
|
+
pass
|
|
374
407
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
408
|
+
else:
|
|
409
|
+
|
|
410
|
+
class Page(PageBase):
|
|
411
|
+
"""Desktop stub — no native runtime available.
|
|
412
|
+
|
|
413
|
+
Fully functional for testing with a mock backend via
|
|
414
|
+
``native_views.set_registry()``.
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
def __init__(self, native_instance: Any = None) -> None:
|
|
418
|
+
super().__init__()
|
|
419
|
+
self.native_instance = native_instance
|
|
420
|
+
_init_page_common(self)
|
|
421
|
+
|
|
422
|
+
def render(self) -> Any:
|
|
423
|
+
raise NotImplementedError("Page subclass must implement render()")
|
|
424
|
+
|
|
425
|
+
def set_state(self, **updates: Any) -> None:
|
|
426
|
+
_set_state(self, **updates)
|
|
427
|
+
|
|
428
|
+
def on_create(self) -> None:
|
|
429
|
+
_on_create(self)
|
|
430
|
+
|
|
431
|
+
def on_start(self) -> None:
|
|
382
432
|
pass
|
|
383
|
-
# On iOS, `navigationController` is exposed as a property; treat it as such.
|
|
384
|
-
nav = getattr(self.native_instance, "navigationController", None)
|
|
385
|
-
if nav is None:
|
|
386
|
-
# If no navigation controller, this push will be a no-op; rely on template to embed one.
|
|
387
|
-
raise RuntimeError(
|
|
388
|
-
"No UINavigationController available; ensure template embeds root in navigation controller"
|
|
389
|
-
)
|
|
390
|
-
# Method name maps from pushViewController:animated:
|
|
391
|
-
nav.pushViewController_animated_(next_vc, True)
|
|
392
433
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
434
|
+
def on_resume(self) -> None:
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
def on_pause(self) -> None:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
def on_stop(self) -> None:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
def on_destroy(self) -> None:
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
def on_restart(self) -> None:
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
def on_save_instance_state(self) -> None:
|
|
450
|
+
pass
|
|
451
|
+
|
|
452
|
+
def on_restore_instance_state(self) -> None:
|
|
453
|
+
pass
|
|
454
|
+
|
|
455
|
+
def set_args(self, args: Optional[dict]) -> None:
|
|
456
|
+
_set_args(self, args)
|
|
457
|
+
|
|
458
|
+
def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
|
|
459
|
+
raise RuntimeError("push() requires a native runtime (iOS or Android)")
|
|
460
|
+
|
|
461
|
+
def pop(self) -> None:
|
|
462
|
+
raise RuntimeError("pop() requires a native runtime (iOS or Android)")
|
|
463
|
+
|
|
464
|
+
def _attach_root(self, native_view: Any) -> None:
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
def _detach_root(self, native_view: Any) -> None:
|
|
468
|
+
pass
|