pythonnative 0.4.0__py3-none-any.whl → 0.6.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 +94 -66
- pythonnative/cli/pn.py +153 -24
- pythonnative/components.py +563 -0
- pythonnative/element.py +53 -0
- pythonnative/hooks.py +287 -0
- pythonnative/hot_reload.py +143 -0
- pythonnative/native_modules/__init__.py +19 -0
- pythonnative/native_modules/camera.py +105 -0
- pythonnative/native_modules/file_system.py +131 -0
- pythonnative/native_modules/location.py +61 -0
- pythonnative/native_modules/notifications.py +151 -0
- pythonnative/native_views.py +1334 -0
- pythonnative/page.py +320 -247
- pythonnative/reconciler.py +262 -0
- pythonnative/style.py +115 -0
- pythonnative/templates/android_template/app/build.gradle +2 -7
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -1
- pythonnative/templates/android_template/build.gradle +1 -1
- pythonnative/utils.py +21 -29
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/METADATA +20 -19
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/RECORD +25 -40
- pythonnative/activity_indicator_view.py +0 -71
- pythonnative/button.py +0 -113
- pythonnative/collection_view.py +0 -0
- pythonnative/date_picker.py +0 -76
- pythonnative/image_view.py +0 -78
- pythonnative/label.py +0 -133
- pythonnative/list_view.py +0 -76
- pythonnative/material_activity_indicator_view.py +0 -71
- pythonnative/material_bottom_navigation_view.py +0 -0
- pythonnative/material_button.py +0 -69
- pythonnative/material_date_picker.py +0 -87
- pythonnative/material_progress_view.py +0 -70
- pythonnative/material_search_bar.py +0 -69
- pythonnative/material_switch.py +0 -69
- pythonnative/material_time_picker.py +0 -76
- pythonnative/material_toolbar.py +0 -0
- pythonnative/picker_view.py +0 -69
- pythonnative/progress_view.py +0 -70
- pythonnative/scroll_view.py +0 -101
- pythonnative/search_bar.py +0 -69
- pythonnative/stack_view.py +0 -199
- pythonnative/switch.py +0 -68
- pythonnative/text_field.py +0 -132
- pythonnative/text_view.py +0 -135
- pythonnative/time_picker.py +0 -77
- pythonnative/view.py +0 -173
- pythonnative/web_view.py +0 -60
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.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,183 @@ 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
|
-
# Back-compat: navigate_to delegates to push
|
|
107
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
|
+
page._reconciler._page_re_render = lambda: _re_render(page)
|
|
126
|
+
element = page.render()
|
|
127
|
+
page._root_native_view = page._reconciler.mount(element)
|
|
128
|
+
page._attach_root(page._root_native_view)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _re_render(page: Any) -> None:
|
|
132
|
+
element = page.render()
|
|
133
|
+
new_root = page._reconciler.reconcile(element)
|
|
134
|
+
if new_root is not page._root_native_view:
|
|
135
|
+
page._detach_root(page._root_native_view)
|
|
136
|
+
page._root_native_view = new_root
|
|
137
|
+
page._attach_root(new_root)
|
|
138
|
+
|
|
117
139
|
|
|
140
|
+
def _resolve_page_path(page_ref: Union[str, Any]) -> str:
|
|
141
|
+
if isinstance(page_ref, str):
|
|
142
|
+
return page_ref
|
|
143
|
+
module = getattr(page_ref, "__module__", None)
|
|
144
|
+
name = getattr(page_ref, "__name__", None)
|
|
145
|
+
if module and name:
|
|
146
|
+
return f"{module}.{name}"
|
|
147
|
+
cls = page_ref.__class__
|
|
148
|
+
return f"{cls.__module__}.{cls.__name__}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _set_args(page: Any, args: Optional[dict]) -> None:
|
|
152
|
+
if isinstance(args, str):
|
|
153
|
+
try:
|
|
154
|
+
page._args = json.loads(args) or {}
|
|
155
|
+
except Exception:
|
|
156
|
+
page._args = {}
|
|
157
|
+
return
|
|
158
|
+
page._args = args or {}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ======================================================================
|
|
162
|
+
# Platform implementations
|
|
163
|
+
# ======================================================================
|
|
164
|
+
|
|
165
|
+
if IS_ANDROID:
|
|
118
166
|
from java import jclass
|
|
119
167
|
|
|
120
|
-
class Page(PageBase
|
|
168
|
+
class Page(PageBase):
|
|
169
|
+
"""Android Page backed by an Activity and Fragment navigation."""
|
|
170
|
+
|
|
121
171
|
def __init__(self, native_instance: Any) -> None:
|
|
122
172
|
super().__init__()
|
|
123
173
|
self.native_class = jclass("android.app.Activity")
|
|
124
174
|
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
175
|
set_android_context(native_instance)
|
|
128
|
-
self
|
|
176
|
+
_init_page_common(self)
|
|
129
177
|
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
try:
|
|
133
|
-
from .utils import get_android_fragment_container
|
|
178
|
+
def render(self) -> Any:
|
|
179
|
+
raise NotImplementedError("Page subclass must implement render()")
|
|
134
180
|
|
|
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)
|
|
181
|
+
def set_state(self, **updates: Any) -> None:
|
|
182
|
+
_set_state(self, **updates)
|
|
145
183
|
|
|
146
184
|
def on_create(self) -> None:
|
|
147
|
-
|
|
185
|
+
_on_create(self)
|
|
148
186
|
|
|
149
187
|
def on_start(self) -> None:
|
|
150
|
-
|
|
188
|
+
pass
|
|
151
189
|
|
|
152
190
|
def on_resume(self) -> None:
|
|
153
|
-
|
|
191
|
+
pass
|
|
154
192
|
|
|
155
193
|
def on_pause(self) -> None:
|
|
156
|
-
|
|
194
|
+
pass
|
|
157
195
|
|
|
158
196
|
def on_stop(self) -> None:
|
|
159
|
-
|
|
197
|
+
pass
|
|
160
198
|
|
|
161
199
|
def on_destroy(self) -> None:
|
|
162
|
-
|
|
200
|
+
pass
|
|
163
201
|
|
|
164
202
|
def on_restart(self) -> None:
|
|
165
|
-
|
|
203
|
+
pass
|
|
166
204
|
|
|
167
205
|
def on_save_instance_state(self) -> None:
|
|
168
|
-
|
|
206
|
+
pass
|
|
169
207
|
|
|
170
208
|
def on_restore_instance_state(self) -> None:
|
|
171
|
-
|
|
209
|
+
pass
|
|
172
210
|
|
|
173
211
|
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")
|
|
212
|
+
_set_args(self, args)
|
|
198
213
|
|
|
199
214
|
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
|
|
215
|
+
page_path = _resolve_page_path(page)
|
|
216
|
+
Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
217
|
+
args_json = json.dumps(args) if args else None
|
|
218
|
+
Navigator.push(self.native_instance, page_path, args_json)
|
|
209
219
|
|
|
210
220
|
def pop(self) -> None:
|
|
211
|
-
# Delegate to Navigator.pop for back-stack pop
|
|
212
221
|
try:
|
|
213
222
|
Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
214
223
|
Navigator.pop(self.native_instance)
|
|
215
224
|
except Exception:
|
|
225
|
+
self.native_instance.finish()
|
|
226
|
+
|
|
227
|
+
def _attach_root(self, native_view: Any) -> None:
|
|
228
|
+
try:
|
|
229
|
+
from .utils import get_android_fragment_container
|
|
230
|
+
|
|
231
|
+
container = get_android_fragment_container()
|
|
216
232
|
try:
|
|
217
|
-
|
|
233
|
+
container.removeAllViews()
|
|
218
234
|
except Exception:
|
|
219
235
|
pass
|
|
236
|
+
LayoutParams = jclass("android.view.ViewGroup$LayoutParams")
|
|
237
|
+
lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
238
|
+
container.addView(native_view, lp)
|
|
239
|
+
except Exception:
|
|
240
|
+
self.native_instance.setContentView(native_view)
|
|
241
|
+
|
|
242
|
+
def _detach_root(self, native_view: Any) -> None:
|
|
243
|
+
try:
|
|
244
|
+
from .utils import get_android_fragment_container
|
|
245
|
+
|
|
246
|
+
container = get_android_fragment_container()
|
|
247
|
+
container.removeAllViews()
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
220
250
|
|
|
221
251
|
else:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
252
|
+
from typing import Dict as _Dict
|
|
253
|
+
|
|
254
|
+
_rubicon_available = False
|
|
255
|
+
try:
|
|
256
|
+
from rubicon.objc import ObjCClass, ObjCInstance
|
|
226
257
|
|
|
227
|
-
|
|
258
|
+
_rubicon_available = True
|
|
228
259
|
|
|
229
|
-
|
|
260
|
+
import gc as _gc
|
|
230
261
|
|
|
231
|
-
|
|
232
|
-
|
|
262
|
+
_gc.disable()
|
|
263
|
+
except ImportError:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
_IOS_PAGE_REGISTRY: _Dict[int, Any] = {}
|
|
233
267
|
|
|
234
268
|
def _ios_register_page(vc_instance: Any, page_obj: Any) -> None:
|
|
235
269
|
try:
|
|
236
|
-
ptr = int(vc_instance.ptr)
|
|
270
|
+
ptr = int(vc_instance.ptr)
|
|
237
271
|
_IOS_PAGE_REGISTRY[ptr] = page_obj
|
|
238
272
|
except Exception:
|
|
239
273
|
pass
|
|
@@ -246,151 +280,190 @@ else:
|
|
|
246
280
|
pass
|
|
247
281
|
|
|
248
282
|
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
|
-
"""
|
|
283
|
+
"""Forward a lifecycle event from Swift ViewController to the registered Page."""
|
|
255
284
|
page = _IOS_PAGE_REGISTRY.get(int(native_addr))
|
|
256
|
-
if
|
|
285
|
+
if page is None:
|
|
257
286
|
return
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
handler()
|
|
262
|
-
except Exception:
|
|
263
|
-
# Avoid surfacing exceptions across the Swift/Python boundary in lifecycle
|
|
264
|
-
pass
|
|
287
|
+
handler = getattr(page, event, None)
|
|
288
|
+
if handler:
|
|
289
|
+
handler()
|
|
265
290
|
|
|
266
|
-
|
|
267
|
-
def __init__(self, native_instance: Any) -> 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: Any) -> 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)
|
|
291
|
+
if _rubicon_available:
|
|
295
292
|
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
class Page(PageBase):
|
|
294
|
+
"""iOS Page backed by a UIViewController."""
|
|
298
295
|
|
|
299
|
-
|
|
300
|
-
|
|
296
|
+
def __init__(self, native_instance: Any) -> None:
|
|
297
|
+
super().__init__()
|
|
298
|
+
self.native_class = ObjCClass("UIViewController")
|
|
299
|
+
if isinstance(native_instance, int):
|
|
300
|
+
try:
|
|
301
|
+
native_instance = ObjCInstance(native_instance)
|
|
302
|
+
except Exception:
|
|
303
|
+
native_instance = None
|
|
304
|
+
self.native_instance = native_instance
|
|
305
|
+
_init_page_common(self)
|
|
306
|
+
if self.native_instance is not None:
|
|
307
|
+
_ios_register_page(self.native_instance, self)
|
|
301
308
|
|
|
302
|
-
|
|
303
|
-
|
|
309
|
+
def render(self) -> Any:
|
|
310
|
+
raise NotImplementedError("Page subclass must implement render()")
|
|
304
311
|
|
|
305
|
-
|
|
306
|
-
|
|
312
|
+
def set_state(self, **updates: Any) -> None:
|
|
313
|
+
_set_state(self, **updates)
|
|
307
314
|
|
|
308
|
-
|
|
309
|
-
|
|
315
|
+
def on_create(self) -> None:
|
|
316
|
+
_on_create(self)
|
|
310
317
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if self.native_instance is not None:
|
|
314
|
-
_ios_unregister_page(self.native_instance)
|
|
318
|
+
def on_start(self) -> None:
|
|
319
|
+
pass
|
|
315
320
|
|
|
316
|
-
|
|
317
|
-
|
|
321
|
+
def on_resume(self) -> None:
|
|
322
|
+
pass
|
|
318
323
|
|
|
319
|
-
|
|
320
|
-
|
|
324
|
+
def on_pause(self) -> None:
|
|
325
|
+
pass
|
|
321
326
|
|
|
322
|
-
|
|
323
|
-
|
|
327
|
+
def on_stop(self) -> None:
|
|
328
|
+
pass
|
|
324
329
|
|
|
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 {}
|
|
330
|
+
def on_destroy(self) -> None:
|
|
331
|
+
if self.native_instance is not None:
|
|
332
|
+
_ios_unregister_page(self.native_instance)
|
|
334
333
|
|
|
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")
|
|
334
|
+
def on_restart(self) -> None:
|
|
335
|
+
pass
|
|
347
336
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
337
|
+
def on_save_instance_state(self) -> None:
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
def on_restore_instance_state(self) -> None:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
def set_args(self, args: Optional[dict]) -> None:
|
|
344
|
+
_set_args(self, args)
|
|
345
|
+
|
|
346
|
+
def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
|
|
347
|
+
page_path = _resolve_page_path(page)
|
|
348
|
+
ViewController = None
|
|
356
349
|
try:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
module_name = None
|
|
350
|
+
ViewController = ObjCClass("ViewController")
|
|
351
|
+
except Exception:
|
|
360
352
|
try:
|
|
361
|
-
|
|
353
|
+
NSBundle = ObjCClass("NSBundle")
|
|
354
|
+
bundle = NSBundle.mainBundle
|
|
362
355
|
module_name = bundle.objectForInfoDictionaryKey_("CFBundleName")
|
|
363
356
|
if module_name is None:
|
|
364
357
|
module_name = bundle.objectForInfoDictionaryKey_("CFBundleExecutable")
|
|
358
|
+
if module_name:
|
|
359
|
+
ViewController = ObjCClass(f"{module_name}.ViewController")
|
|
365
360
|
except Exception:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
361
|
+
pass
|
|
362
|
+
|
|
363
|
+
if ViewController is None:
|
|
364
|
+
raise NameError("ViewController class not found; ensure Swift class is ObjC-visible")
|
|
365
|
+
|
|
366
|
+
next_vc = ViewController.alloc().init()
|
|
367
|
+
try:
|
|
368
|
+
next_vc.setValue_forKey_(page_path, "requestedPagePath")
|
|
369
|
+
if args:
|
|
370
|
+
next_vc.setValue_forKey_(json.dumps(args), "requestedPageArgsJSON")
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
nav = getattr(self.native_instance, "navigationController", None)
|
|
374
|
+
if nav is None:
|
|
375
|
+
raise RuntimeError(
|
|
376
|
+
"No UINavigationController available; ensure template embeds root in navigation controller"
|
|
377
|
+
)
|
|
378
|
+
nav.pushViewController_animated_(next_vc, True)
|
|
379
|
+
|
|
380
|
+
def pop(self) -> None:
|
|
381
|
+
nav = getattr(self.native_instance, "navigationController", None)
|
|
382
|
+
if nav is not None:
|
|
383
|
+
nav.popViewControllerAnimated_(True)
|
|
384
|
+
|
|
385
|
+
def _attach_root(self, native_view: Any) -> None:
|
|
386
|
+
root_view = self.native_instance.view
|
|
387
|
+
native_view.setTranslatesAutoresizingMaskIntoConstraints_(False)
|
|
388
|
+
root_view.addSubview_(native_view)
|
|
389
|
+
try:
|
|
390
|
+
safe = root_view.safeAreaLayoutGuide
|
|
391
|
+
native_view.topAnchor.constraintEqualToAnchor_(safe.topAnchor).setActive_(True)
|
|
392
|
+
native_view.bottomAnchor.constraintEqualToAnchor_(safe.bottomAnchor).setActive_(True)
|
|
393
|
+
native_view.leadingAnchor.constraintEqualToAnchor_(safe.leadingAnchor).setActive_(True)
|
|
394
|
+
native_view.trailingAnchor.constraintEqualToAnchor_(safe.trailingAnchor).setActive_(True)
|
|
369
395
|
except Exception:
|
|
370
|
-
|
|
396
|
+
native_view.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
397
|
+
try:
|
|
398
|
+
native_view.setFrame_(root_view.bounds)
|
|
399
|
+
native_view.setAutoresizingMask_(2 | 16)
|
|
400
|
+
except Exception:
|
|
401
|
+
pass
|
|
371
402
|
|
|
372
|
-
|
|
373
|
-
|
|
403
|
+
def _detach_root(self, native_view: Any) -> None:
|
|
404
|
+
try:
|
|
405
|
+
native_view.removeFromSuperview()
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
374
408
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
409
|
+
else:
|
|
410
|
+
|
|
411
|
+
class Page(PageBase):
|
|
412
|
+
"""Desktop stub — no native runtime available.
|
|
413
|
+
|
|
414
|
+
Fully functional for testing with a mock backend via
|
|
415
|
+
``native_views.set_registry()``.
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
def __init__(self, native_instance: Any = None) -> None:
|
|
419
|
+
super().__init__()
|
|
420
|
+
self.native_instance = native_instance
|
|
421
|
+
_init_page_common(self)
|
|
422
|
+
|
|
423
|
+
def render(self) -> Any:
|
|
424
|
+
raise NotImplementedError("Page subclass must implement render()")
|
|
425
|
+
|
|
426
|
+
def set_state(self, **updates: Any) -> None:
|
|
427
|
+
_set_state(self, **updates)
|
|
428
|
+
|
|
429
|
+
def on_create(self) -> None:
|
|
430
|
+
_on_create(self)
|
|
431
|
+
|
|
432
|
+
def on_start(self) -> None:
|
|
382
433
|
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
434
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
435
|
+
def on_resume(self) -> None:
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
def on_pause(self) -> None:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
def on_stop(self) -> None:
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
def on_destroy(self) -> None:
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
def on_restart(self) -> None:
|
|
448
|
+
pass
|
|
449
|
+
|
|
450
|
+
def on_save_instance_state(self) -> None:
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
def on_restore_instance_state(self) -> None:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
def set_args(self, args: Optional[dict]) -> None:
|
|
457
|
+
_set_args(self, args)
|
|
458
|
+
|
|
459
|
+
def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
|
|
460
|
+
raise RuntimeError("push() requires a native runtime (iOS or Android)")
|
|
461
|
+
|
|
462
|
+
def pop(self) -> None:
|
|
463
|
+
raise RuntimeError("pop() requires a native runtime (iOS or Android)")
|
|
464
|
+
|
|
465
|
+
def _attach_root(self, native_view: Any) -> None:
|
|
466
|
+
pass
|
|
467
|
+
|
|
468
|
+
def _detach_root(self, native_view: Any) -> None:
|
|
469
|
+
pass
|