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
|
@@ -0,0 +1,1334 @@
|
|
|
1
|
+
"""Platform-specific native view creation and update logic.
|
|
2
|
+
|
|
3
|
+
This module replaces the old per-widget files. All platform-branching
|
|
4
|
+
lives here, guarded behind lazy imports so the module can be imported
|
|
5
|
+
on desktop for testing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Callable, Dict, Optional, Union
|
|
9
|
+
|
|
10
|
+
from .utils import IS_ANDROID
|
|
11
|
+
|
|
12
|
+
# ======================================================================
|
|
13
|
+
# Abstract handler protocol
|
|
14
|
+
# ======================================================================
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ViewHandler:
|
|
18
|
+
"""Protocol for creating, updating, and managing children of a native view type."""
|
|
19
|
+
|
|
20
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
def update(self, native_view: Any, changed_props: Dict[str, Any]) -> None:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
33
|
+
self.add_child(parent, child)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ======================================================================
|
|
37
|
+
# Registry
|
|
38
|
+
# ======================================================================
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NativeViewRegistry:
|
|
42
|
+
"""Maps element type names to platform-specific :class:`ViewHandler` instances."""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self._handlers: Dict[str, ViewHandler] = {}
|
|
46
|
+
|
|
47
|
+
def register(self, type_name: str, handler: ViewHandler) -> None:
|
|
48
|
+
self._handlers[type_name] = handler
|
|
49
|
+
|
|
50
|
+
def create_view(self, type_name: str, props: Dict[str, Any]) -> Any:
|
|
51
|
+
handler = self._handlers.get(type_name)
|
|
52
|
+
if handler is None:
|
|
53
|
+
raise ValueError(f"Unknown element type: {type_name!r}")
|
|
54
|
+
return handler.create(props)
|
|
55
|
+
|
|
56
|
+
def update_view(self, native_view: Any, type_name: str, changed_props: Dict[str, Any]) -> None:
|
|
57
|
+
handler = self._handlers.get(type_name)
|
|
58
|
+
if handler is not None:
|
|
59
|
+
handler.update(native_view, changed_props)
|
|
60
|
+
|
|
61
|
+
def add_child(self, parent: Any, child: Any, parent_type: str) -> None:
|
|
62
|
+
handler = self._handlers.get(parent_type)
|
|
63
|
+
if handler is not None:
|
|
64
|
+
handler.add_child(parent, child)
|
|
65
|
+
|
|
66
|
+
def remove_child(self, parent: Any, child: Any, parent_type: str) -> None:
|
|
67
|
+
handler = self._handlers.get(parent_type)
|
|
68
|
+
if handler is not None:
|
|
69
|
+
handler.remove_child(parent, child)
|
|
70
|
+
|
|
71
|
+
def insert_child(self, parent: Any, child: Any, parent_type: str, index: int) -> None:
|
|
72
|
+
handler = self._handlers.get(parent_type)
|
|
73
|
+
if handler is not None:
|
|
74
|
+
handler.insert_child(parent, child, index)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ======================================================================
|
|
78
|
+
# Shared helpers
|
|
79
|
+
# ======================================================================
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_color_int(color: Union[str, int]) -> int:
|
|
83
|
+
"""Parse ``#RRGGBB`` / ``#AARRGGBB`` hex string or raw int to a *signed* ARGB int.
|
|
84
|
+
|
|
85
|
+
Java's ``setBackgroundColor`` et al. expect a signed 32-bit int, so values
|
|
86
|
+
with a high alpha byte (e.g. 0xFF…) must be converted to negative ints.
|
|
87
|
+
"""
|
|
88
|
+
if isinstance(color, int):
|
|
89
|
+
val = color
|
|
90
|
+
else:
|
|
91
|
+
c = color.strip().lstrip("#")
|
|
92
|
+
if len(c) == 6:
|
|
93
|
+
c = "FF" + c
|
|
94
|
+
val = int(c, 16)
|
|
95
|
+
if val > 0x7FFFFFFF:
|
|
96
|
+
val -= 0x100000000
|
|
97
|
+
return val
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_padding(
|
|
101
|
+
padding: Any,
|
|
102
|
+
) -> tuple:
|
|
103
|
+
"""Normalise various padding representations to ``(left, top, right, bottom)``."""
|
|
104
|
+
if padding is None:
|
|
105
|
+
return (0, 0, 0, 0)
|
|
106
|
+
if isinstance(padding, (int, float)):
|
|
107
|
+
v = int(padding)
|
|
108
|
+
return (v, v, v, v)
|
|
109
|
+
if isinstance(padding, dict):
|
|
110
|
+
h = int(padding.get("horizontal", 0))
|
|
111
|
+
v = int(padding.get("vertical", 0))
|
|
112
|
+
left = int(padding.get("left", h))
|
|
113
|
+
right = int(padding.get("right", h))
|
|
114
|
+
top = int(padding.get("top", v))
|
|
115
|
+
bottom = int(padding.get("bottom", v))
|
|
116
|
+
a = int(padding.get("all", 0))
|
|
117
|
+
if a:
|
|
118
|
+
left = left or a
|
|
119
|
+
right = right or a
|
|
120
|
+
top = top or a
|
|
121
|
+
bottom = bottom or a
|
|
122
|
+
return (left, top, right, bottom)
|
|
123
|
+
return (0, 0, 0, 0)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
_LAYOUT_KEYS = frozenset(
|
|
127
|
+
{
|
|
128
|
+
"width",
|
|
129
|
+
"height",
|
|
130
|
+
"flex",
|
|
131
|
+
"margin",
|
|
132
|
+
"min_width",
|
|
133
|
+
"max_width",
|
|
134
|
+
"min_height",
|
|
135
|
+
"max_height",
|
|
136
|
+
"align_self",
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ======================================================================
|
|
142
|
+
# Platform handler registration (lazy imports inside functions)
|
|
143
|
+
# ======================================================================
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
147
|
+
from java import dynamic_proxy, jclass
|
|
148
|
+
|
|
149
|
+
from .utils import get_android_context
|
|
150
|
+
|
|
151
|
+
def _ctx() -> Any:
|
|
152
|
+
return get_android_context()
|
|
153
|
+
|
|
154
|
+
def _density() -> float:
|
|
155
|
+
return float(_ctx().getResources().getDisplayMetrics().density)
|
|
156
|
+
|
|
157
|
+
def _dp(value: float) -> int:
|
|
158
|
+
return int(value * _density())
|
|
159
|
+
|
|
160
|
+
def _apply_layout(view: Any, props: Dict[str, Any]) -> None:
|
|
161
|
+
"""Apply common layout properties to an Android view."""
|
|
162
|
+
lp = view.getLayoutParams()
|
|
163
|
+
LayoutParams = jclass("android.widget.LinearLayout$LayoutParams")
|
|
164
|
+
ViewGroupLP = jclass("android.view.ViewGroup$LayoutParams")
|
|
165
|
+
needs_set = False
|
|
166
|
+
|
|
167
|
+
if lp is None:
|
|
168
|
+
lp = LayoutParams(ViewGroupLP.WRAP_CONTENT, ViewGroupLP.WRAP_CONTENT)
|
|
169
|
+
needs_set = True
|
|
170
|
+
|
|
171
|
+
if "width" in props and props["width"] is not None:
|
|
172
|
+
lp.width = _dp(float(props["width"]))
|
|
173
|
+
needs_set = True
|
|
174
|
+
if "height" in props and props["height"] is not None:
|
|
175
|
+
lp.height = _dp(float(props["height"]))
|
|
176
|
+
needs_set = True
|
|
177
|
+
if "flex" in props and props["flex"] is not None:
|
|
178
|
+
try:
|
|
179
|
+
lp.weight = float(props["flex"])
|
|
180
|
+
needs_set = True
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
if "margin" in props and props["margin"] is not None:
|
|
184
|
+
left, top, right, bottom = _resolve_padding(props["margin"])
|
|
185
|
+
try:
|
|
186
|
+
lp.setMargins(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
187
|
+
needs_set = True
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
if needs_set:
|
|
192
|
+
view.setLayoutParams(lp)
|
|
193
|
+
|
|
194
|
+
# ---- Text -----------------------------------------------------------
|
|
195
|
+
class AndroidTextHandler(ViewHandler):
|
|
196
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
197
|
+
tv = jclass("android.widget.TextView")(_ctx())
|
|
198
|
+
self._apply(tv, props)
|
|
199
|
+
_apply_layout(tv, props)
|
|
200
|
+
return tv
|
|
201
|
+
|
|
202
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
203
|
+
self._apply(native_view, changed)
|
|
204
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
205
|
+
_apply_layout(native_view, changed)
|
|
206
|
+
|
|
207
|
+
def _apply(self, tv: Any, props: Dict[str, Any]) -> None:
|
|
208
|
+
if "text" in props:
|
|
209
|
+
tv.setText(str(props["text"]))
|
|
210
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
211
|
+
tv.setTextSize(float(props["font_size"]))
|
|
212
|
+
if "color" in props and props["color"] is not None:
|
|
213
|
+
tv.setTextColor(parse_color_int(props["color"]))
|
|
214
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
215
|
+
tv.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
216
|
+
if "bold" in props and props["bold"]:
|
|
217
|
+
tv.setTypeface(tv.getTypeface(), 1) # Typeface.BOLD = 1
|
|
218
|
+
if "max_lines" in props and props["max_lines"] is not None:
|
|
219
|
+
tv.setMaxLines(int(props["max_lines"]))
|
|
220
|
+
if "text_align" in props:
|
|
221
|
+
Gravity = jclass("android.view.Gravity")
|
|
222
|
+
mapping = {"left": Gravity.START, "center": Gravity.CENTER, "right": Gravity.END}
|
|
223
|
+
tv.setGravity(mapping.get(props["text_align"], Gravity.START))
|
|
224
|
+
|
|
225
|
+
# ---- Button ---------------------------------------------------------
|
|
226
|
+
class AndroidButtonHandler(ViewHandler):
|
|
227
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
228
|
+
btn = jclass("android.widget.Button")(_ctx())
|
|
229
|
+
self._apply(btn, props)
|
|
230
|
+
_apply_layout(btn, props)
|
|
231
|
+
return btn
|
|
232
|
+
|
|
233
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
234
|
+
self._apply(native_view, changed)
|
|
235
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
236
|
+
_apply_layout(native_view, changed)
|
|
237
|
+
|
|
238
|
+
def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
|
|
239
|
+
if "title" in props:
|
|
240
|
+
btn.setText(str(props["title"]))
|
|
241
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
242
|
+
btn.setTextSize(float(props["font_size"]))
|
|
243
|
+
if "color" in props and props["color"] is not None:
|
|
244
|
+
btn.setTextColor(parse_color_int(props["color"]))
|
|
245
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
246
|
+
btn.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
247
|
+
if "enabled" in props:
|
|
248
|
+
btn.setEnabled(bool(props["enabled"]))
|
|
249
|
+
if "on_click" in props:
|
|
250
|
+
cb = props["on_click"]
|
|
251
|
+
if cb is not None:
|
|
252
|
+
|
|
253
|
+
class ClickProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)):
|
|
254
|
+
def __init__(self, callback: Callable[[], None]) -> None:
|
|
255
|
+
super().__init__()
|
|
256
|
+
self.callback = callback
|
|
257
|
+
|
|
258
|
+
def onClick(self, view: Any) -> None:
|
|
259
|
+
self.callback()
|
|
260
|
+
|
|
261
|
+
btn.setOnClickListener(ClickProxy(cb))
|
|
262
|
+
else:
|
|
263
|
+
btn.setOnClickListener(None)
|
|
264
|
+
|
|
265
|
+
# ---- Column (vertical LinearLayout) ---------------------------------
|
|
266
|
+
class AndroidColumnHandler(ViewHandler):
|
|
267
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
268
|
+
ll = jclass("android.widget.LinearLayout")(_ctx())
|
|
269
|
+
ll.setOrientation(jclass("android.widget.LinearLayout").VERTICAL)
|
|
270
|
+
self._apply(ll, props)
|
|
271
|
+
_apply_layout(ll, props)
|
|
272
|
+
return ll
|
|
273
|
+
|
|
274
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
275
|
+
self._apply(native_view, changed)
|
|
276
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
277
|
+
_apply_layout(native_view, changed)
|
|
278
|
+
|
|
279
|
+
def _apply(self, ll: Any, props: Dict[str, Any]) -> None:
|
|
280
|
+
if "spacing" in props and props["spacing"]:
|
|
281
|
+
px = _dp(float(props["spacing"]))
|
|
282
|
+
GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
|
|
283
|
+
d = GradientDrawable()
|
|
284
|
+
d.setColor(0x00000000)
|
|
285
|
+
d.setSize(1, px)
|
|
286
|
+
ll.setShowDividers(jclass("android.widget.LinearLayout").SHOW_DIVIDER_MIDDLE)
|
|
287
|
+
ll.setDividerDrawable(d)
|
|
288
|
+
if "padding" in props:
|
|
289
|
+
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
290
|
+
ll.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
291
|
+
if "alignment" in props and props["alignment"]:
|
|
292
|
+
Gravity = jclass("android.view.Gravity")
|
|
293
|
+
mapping = {
|
|
294
|
+
"fill": Gravity.FILL_HORIZONTAL,
|
|
295
|
+
"center": Gravity.CENTER_HORIZONTAL,
|
|
296
|
+
"leading": Gravity.START,
|
|
297
|
+
"start": Gravity.START,
|
|
298
|
+
"trailing": Gravity.END,
|
|
299
|
+
"end": Gravity.END,
|
|
300
|
+
}
|
|
301
|
+
ll.setGravity(mapping.get(props["alignment"], Gravity.FILL_HORIZONTAL))
|
|
302
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
303
|
+
ll.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
304
|
+
|
|
305
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
306
|
+
parent.addView(child)
|
|
307
|
+
|
|
308
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
309
|
+
parent.removeView(child)
|
|
310
|
+
|
|
311
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
312
|
+
parent.addView(child, index)
|
|
313
|
+
|
|
314
|
+
# ---- Row (horizontal LinearLayout) ----------------------------------
|
|
315
|
+
class AndroidRowHandler(ViewHandler):
|
|
316
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
317
|
+
ll = jclass("android.widget.LinearLayout")(_ctx())
|
|
318
|
+
ll.setOrientation(jclass("android.widget.LinearLayout").HORIZONTAL)
|
|
319
|
+
self._apply(ll, props)
|
|
320
|
+
_apply_layout(ll, props)
|
|
321
|
+
return ll
|
|
322
|
+
|
|
323
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
324
|
+
self._apply(native_view, changed)
|
|
325
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
326
|
+
_apply_layout(native_view, changed)
|
|
327
|
+
|
|
328
|
+
def _apply(self, ll: Any, props: Dict[str, Any]) -> None:
|
|
329
|
+
if "spacing" in props and props["spacing"]:
|
|
330
|
+
px = _dp(float(props["spacing"]))
|
|
331
|
+
GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
|
|
332
|
+
d = GradientDrawable()
|
|
333
|
+
d.setColor(0x00000000)
|
|
334
|
+
d.setSize(px, 1)
|
|
335
|
+
ll.setShowDividers(jclass("android.widget.LinearLayout").SHOW_DIVIDER_MIDDLE)
|
|
336
|
+
ll.setDividerDrawable(d)
|
|
337
|
+
if "padding" in props:
|
|
338
|
+
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
339
|
+
ll.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
340
|
+
if "alignment" in props and props["alignment"]:
|
|
341
|
+
Gravity = jclass("android.view.Gravity")
|
|
342
|
+
mapping = {
|
|
343
|
+
"fill": Gravity.FILL_VERTICAL,
|
|
344
|
+
"center": Gravity.CENTER_VERTICAL,
|
|
345
|
+
"top": Gravity.TOP,
|
|
346
|
+
"bottom": Gravity.BOTTOM,
|
|
347
|
+
}
|
|
348
|
+
ll.setGravity(mapping.get(props["alignment"], Gravity.FILL_VERTICAL))
|
|
349
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
350
|
+
ll.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
351
|
+
|
|
352
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
353
|
+
parent.addView(child)
|
|
354
|
+
|
|
355
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
356
|
+
parent.removeView(child)
|
|
357
|
+
|
|
358
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
359
|
+
parent.addView(child, index)
|
|
360
|
+
|
|
361
|
+
# ---- ScrollView -----------------------------------------------------
|
|
362
|
+
class AndroidScrollViewHandler(ViewHandler):
|
|
363
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
364
|
+
sv = jclass("android.widget.ScrollView")(_ctx())
|
|
365
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
366
|
+
sv.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
367
|
+
_apply_layout(sv, props)
|
|
368
|
+
return sv
|
|
369
|
+
|
|
370
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
371
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
372
|
+
native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
|
|
373
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
374
|
+
_apply_layout(native_view, changed)
|
|
375
|
+
|
|
376
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
377
|
+
parent.addView(child)
|
|
378
|
+
|
|
379
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
380
|
+
parent.removeView(child)
|
|
381
|
+
|
|
382
|
+
# ---- TextInput (EditText) with on_change ----------------------------
|
|
383
|
+
class AndroidTextInputHandler(ViewHandler):
|
|
384
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
385
|
+
et = jclass("android.widget.EditText")(_ctx())
|
|
386
|
+
self._apply(et, props)
|
|
387
|
+
_apply_layout(et, props)
|
|
388
|
+
return et
|
|
389
|
+
|
|
390
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
391
|
+
self._apply(native_view, changed)
|
|
392
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
393
|
+
_apply_layout(native_view, changed)
|
|
394
|
+
|
|
395
|
+
def _apply(self, et: Any, props: Dict[str, Any]) -> None:
|
|
396
|
+
if "value" in props:
|
|
397
|
+
et.setText(str(props["value"]))
|
|
398
|
+
if "placeholder" in props:
|
|
399
|
+
et.setHint(str(props["placeholder"]))
|
|
400
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
401
|
+
et.setTextSize(float(props["font_size"]))
|
|
402
|
+
if "color" in props and props["color"] is not None:
|
|
403
|
+
et.setTextColor(parse_color_int(props["color"]))
|
|
404
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
405
|
+
et.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
406
|
+
if "secure" in props and props["secure"]:
|
|
407
|
+
InputType = jclass("android.text.InputType")
|
|
408
|
+
et.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
|
|
409
|
+
if "on_change" in props:
|
|
410
|
+
cb = props["on_change"]
|
|
411
|
+
if cb is not None:
|
|
412
|
+
TextWatcher = jclass("android.text.TextWatcher")
|
|
413
|
+
|
|
414
|
+
class ChangeProxy(dynamic_proxy(TextWatcher)):
|
|
415
|
+
def __init__(self, callback: Callable[[str], None]) -> None:
|
|
416
|
+
super().__init__()
|
|
417
|
+
self.callback = callback
|
|
418
|
+
|
|
419
|
+
def afterTextChanged(self, s: Any) -> None:
|
|
420
|
+
self.callback(str(s))
|
|
421
|
+
|
|
422
|
+
def beforeTextChanged(self, s: Any, start: int, count: int, after: int) -> None:
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
def onTextChanged(self, s: Any, start: int, before: int, count: int) -> None:
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
et.addTextChangedListener(ChangeProxy(cb))
|
|
429
|
+
|
|
430
|
+
# ---- Image (with URL loading) ---------------------------------------
|
|
431
|
+
class AndroidImageHandler(ViewHandler):
|
|
432
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
433
|
+
iv = jclass("android.widget.ImageView")(_ctx())
|
|
434
|
+
self._apply(iv, props)
|
|
435
|
+
_apply_layout(iv, props)
|
|
436
|
+
return iv
|
|
437
|
+
|
|
438
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
439
|
+
self._apply(native_view, changed)
|
|
440
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
441
|
+
_apply_layout(native_view, changed)
|
|
442
|
+
|
|
443
|
+
def _apply(self, iv: Any, props: Dict[str, Any]) -> None:
|
|
444
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
445
|
+
iv.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
446
|
+
if "source" in props and props["source"]:
|
|
447
|
+
self._load_source(iv, props["source"])
|
|
448
|
+
if "scale_type" in props and props["scale_type"]:
|
|
449
|
+
ScaleType = jclass("android.widget.ImageView$ScaleType")
|
|
450
|
+
mapping = {
|
|
451
|
+
"cover": ScaleType.CENTER_CROP,
|
|
452
|
+
"contain": ScaleType.FIT_CENTER,
|
|
453
|
+
"stretch": ScaleType.FIT_XY,
|
|
454
|
+
"center": ScaleType.CENTER,
|
|
455
|
+
}
|
|
456
|
+
st = mapping.get(props["scale_type"])
|
|
457
|
+
if st:
|
|
458
|
+
iv.setScaleType(st)
|
|
459
|
+
|
|
460
|
+
def _load_source(self, iv: Any, source: str) -> None:
|
|
461
|
+
try:
|
|
462
|
+
if source.startswith(("http://", "https://")):
|
|
463
|
+
Thread = jclass("java.lang.Thread")
|
|
464
|
+
Runnable = jclass("java.lang.Runnable")
|
|
465
|
+
URL = jclass("java.net.URL")
|
|
466
|
+
BitmapFactory = jclass("android.graphics.BitmapFactory")
|
|
467
|
+
Handler = jclass("android.os.Handler")
|
|
468
|
+
Looper = jclass("android.os.Looper")
|
|
469
|
+
handler = Handler(Looper.getMainLooper())
|
|
470
|
+
|
|
471
|
+
class LoadTask(dynamic_proxy(Runnable)):
|
|
472
|
+
def __init__(self, image_view: Any, url_str: str, main_handler: Any) -> None:
|
|
473
|
+
super().__init__()
|
|
474
|
+
self.image_view = image_view
|
|
475
|
+
self.url_str = url_str
|
|
476
|
+
self.main_handler = main_handler
|
|
477
|
+
|
|
478
|
+
def run(self) -> None:
|
|
479
|
+
try:
|
|
480
|
+
url = URL(self.url_str)
|
|
481
|
+
stream = url.openStream()
|
|
482
|
+
bitmap = BitmapFactory.decodeStream(stream)
|
|
483
|
+
stream.close()
|
|
484
|
+
|
|
485
|
+
class SetImage(dynamic_proxy(Runnable)):
|
|
486
|
+
def __init__(self, view: Any, bmp: Any) -> None:
|
|
487
|
+
super().__init__()
|
|
488
|
+
self.view = view
|
|
489
|
+
self.bmp = bmp
|
|
490
|
+
|
|
491
|
+
def run(self) -> None:
|
|
492
|
+
self.view.setImageBitmap(self.bmp)
|
|
493
|
+
|
|
494
|
+
self.main_handler.post(SetImage(self.image_view, bitmap))
|
|
495
|
+
except Exception:
|
|
496
|
+
pass
|
|
497
|
+
|
|
498
|
+
Thread(LoadTask(iv, source, handler)).start()
|
|
499
|
+
else:
|
|
500
|
+
ctx = _ctx()
|
|
501
|
+
res = ctx.getResources()
|
|
502
|
+
pkg = ctx.getPackageName()
|
|
503
|
+
res_name = source.rsplit(".", 1)[0] if "." in source else source
|
|
504
|
+
res_id = res.getIdentifier(res_name, "drawable", pkg)
|
|
505
|
+
if res_id != 0:
|
|
506
|
+
iv.setImageResource(res_id)
|
|
507
|
+
except Exception:
|
|
508
|
+
pass
|
|
509
|
+
|
|
510
|
+
# ---- Switch (with on_change) ----------------------------------------
|
|
511
|
+
class AndroidSwitchHandler(ViewHandler):
|
|
512
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
513
|
+
sw = jclass("android.widget.Switch")(_ctx())
|
|
514
|
+
self._apply(sw, props)
|
|
515
|
+
_apply_layout(sw, props)
|
|
516
|
+
return sw
|
|
517
|
+
|
|
518
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
519
|
+
self._apply(native_view, changed)
|
|
520
|
+
|
|
521
|
+
def _apply(self, sw: Any, props: Dict[str, Any]) -> None:
|
|
522
|
+
if "value" in props:
|
|
523
|
+
sw.setChecked(bool(props["value"]))
|
|
524
|
+
if "on_change" in props and props["on_change"] is not None:
|
|
525
|
+
cb = props["on_change"]
|
|
526
|
+
|
|
527
|
+
class CheckedProxy(dynamic_proxy(jclass("android.widget.CompoundButton").OnCheckedChangeListener)):
|
|
528
|
+
def __init__(self, callback: Callable[[bool], None]) -> None:
|
|
529
|
+
super().__init__()
|
|
530
|
+
self.callback = callback
|
|
531
|
+
|
|
532
|
+
def onCheckedChanged(self, button: Any, checked: bool) -> None:
|
|
533
|
+
self.callback(checked)
|
|
534
|
+
|
|
535
|
+
sw.setOnCheckedChangeListener(CheckedProxy(cb))
|
|
536
|
+
|
|
537
|
+
# ---- ProgressBar ----------------------------------------------------
|
|
538
|
+
class AndroidProgressBarHandler(ViewHandler):
|
|
539
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
540
|
+
style = jclass("android.R$attr").progressBarStyleHorizontal
|
|
541
|
+
pb = jclass("android.widget.ProgressBar")(_ctx(), None, 0, style)
|
|
542
|
+
pb.setMax(1000)
|
|
543
|
+
self._apply(pb, props)
|
|
544
|
+
_apply_layout(pb, props)
|
|
545
|
+
return pb
|
|
546
|
+
|
|
547
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
548
|
+
self._apply(native_view, changed)
|
|
549
|
+
|
|
550
|
+
def _apply(self, pb: Any, props: Dict[str, Any]) -> None:
|
|
551
|
+
if "value" in props:
|
|
552
|
+
pb.setProgress(int(float(props["value"]) * 1000))
|
|
553
|
+
|
|
554
|
+
# ---- ActivityIndicator (circular ProgressBar) -----------------------
|
|
555
|
+
class AndroidActivityIndicatorHandler(ViewHandler):
|
|
556
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
557
|
+
pb = jclass("android.widget.ProgressBar")(_ctx())
|
|
558
|
+
if not props.get("animating", True):
|
|
559
|
+
pb.setVisibility(jclass("android.view.View").GONE)
|
|
560
|
+
_apply_layout(pb, props)
|
|
561
|
+
return pb
|
|
562
|
+
|
|
563
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
564
|
+
View = jclass("android.view.View")
|
|
565
|
+
if "animating" in changed:
|
|
566
|
+
native_view.setVisibility(View.VISIBLE if changed["animating"] else View.GONE)
|
|
567
|
+
|
|
568
|
+
# ---- WebView --------------------------------------------------------
|
|
569
|
+
class AndroidWebViewHandler(ViewHandler):
|
|
570
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
571
|
+
wv = jclass("android.webkit.WebView")(_ctx())
|
|
572
|
+
if "url" in props and props["url"]:
|
|
573
|
+
wv.loadUrl(str(props["url"]))
|
|
574
|
+
_apply_layout(wv, props)
|
|
575
|
+
return wv
|
|
576
|
+
|
|
577
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
578
|
+
if "url" in changed and changed["url"]:
|
|
579
|
+
native_view.loadUrl(str(changed["url"]))
|
|
580
|
+
|
|
581
|
+
# ---- Spacer ---------------------------------------------------------
|
|
582
|
+
class AndroidSpacerHandler(ViewHandler):
|
|
583
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
584
|
+
v = jclass("android.view.View")(_ctx())
|
|
585
|
+
if "size" in props and props["size"] is not None:
|
|
586
|
+
px = _dp(float(props["size"]))
|
|
587
|
+
lp = jclass("android.widget.LinearLayout$LayoutParams")(px, px)
|
|
588
|
+
v.setLayoutParams(lp)
|
|
589
|
+
if "flex" in props and props["flex"] is not None:
|
|
590
|
+
lp = v.getLayoutParams()
|
|
591
|
+
if lp is None:
|
|
592
|
+
lp = jclass("android.widget.LinearLayout$LayoutParams")(0, 0)
|
|
593
|
+
lp.weight = float(props["flex"])
|
|
594
|
+
v.setLayoutParams(lp)
|
|
595
|
+
return v
|
|
596
|
+
|
|
597
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
598
|
+
if "size" in changed and changed["size"] is not None:
|
|
599
|
+
px = _dp(float(changed["size"]))
|
|
600
|
+
lp = jclass("android.widget.LinearLayout$LayoutParams")(px, px)
|
|
601
|
+
native_view.setLayoutParams(lp)
|
|
602
|
+
|
|
603
|
+
# ---- View (generic container FrameLayout) ---------------------------
|
|
604
|
+
class AndroidViewHandler(ViewHandler):
|
|
605
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
606
|
+
fl = jclass("android.widget.FrameLayout")(_ctx())
|
|
607
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
608
|
+
fl.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
609
|
+
if "padding" in props:
|
|
610
|
+
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
611
|
+
fl.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
612
|
+
_apply_layout(fl, props)
|
|
613
|
+
return fl
|
|
614
|
+
|
|
615
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
616
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
617
|
+
native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
|
|
618
|
+
if "padding" in changed:
|
|
619
|
+
left, top, right, bottom = _resolve_padding(changed["padding"])
|
|
620
|
+
native_view.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
621
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
622
|
+
_apply_layout(native_view, changed)
|
|
623
|
+
|
|
624
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
625
|
+
parent.addView(child)
|
|
626
|
+
|
|
627
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
628
|
+
parent.removeView(child)
|
|
629
|
+
|
|
630
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
631
|
+
parent.addView(child, index)
|
|
632
|
+
|
|
633
|
+
# ---- SafeAreaView (FrameLayout with fitsSystemWindows) ---------------
|
|
634
|
+
class AndroidSafeAreaViewHandler(ViewHandler):
|
|
635
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
636
|
+
fl = jclass("android.widget.FrameLayout")(_ctx())
|
|
637
|
+
fl.setFitsSystemWindows(True)
|
|
638
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
639
|
+
fl.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
640
|
+
if "padding" in props:
|
|
641
|
+
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
642
|
+
fl.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
643
|
+
return fl
|
|
644
|
+
|
|
645
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
646
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
647
|
+
native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
|
|
648
|
+
|
|
649
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
650
|
+
parent.addView(child)
|
|
651
|
+
|
|
652
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
653
|
+
parent.removeView(child)
|
|
654
|
+
|
|
655
|
+
# ---- Modal (AlertDialog) -------------------------------------------
|
|
656
|
+
class AndroidModalHandler(ViewHandler):
|
|
657
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
658
|
+
placeholder = jclass("android.view.View")(_ctx())
|
|
659
|
+
placeholder.setVisibility(jclass("android.view.View").GONE)
|
|
660
|
+
return placeholder
|
|
661
|
+
|
|
662
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
663
|
+
pass
|
|
664
|
+
|
|
665
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
666
|
+
pass
|
|
667
|
+
|
|
668
|
+
# ---- Slider (SeekBar) -----------------------------------------------
|
|
669
|
+
class AndroidSliderHandler(ViewHandler):
|
|
670
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
671
|
+
sb = jclass("android.widget.SeekBar")(_ctx())
|
|
672
|
+
sb.setMax(1000)
|
|
673
|
+
self._apply(sb, props)
|
|
674
|
+
_apply_layout(sb, props)
|
|
675
|
+
return sb
|
|
676
|
+
|
|
677
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
678
|
+
self._apply(native_view, changed)
|
|
679
|
+
|
|
680
|
+
def _apply(self, sb: Any, props: Dict[str, Any]) -> None:
|
|
681
|
+
min_val = float(props.get("min_value", 0))
|
|
682
|
+
max_val = float(props.get("max_value", 1))
|
|
683
|
+
rng = max_val - min_val if max_val != min_val else 1
|
|
684
|
+
if "value" in props:
|
|
685
|
+
normalized = (float(props["value"]) - min_val) / rng
|
|
686
|
+
sb.setProgress(int(normalized * 1000))
|
|
687
|
+
if "on_change" in props and props["on_change"] is not None:
|
|
688
|
+
cb = props["on_change"]
|
|
689
|
+
|
|
690
|
+
class SeekProxy(dynamic_proxy(jclass("android.widget.SeekBar").OnSeekBarChangeListener)):
|
|
691
|
+
def __init__(self, callback: Callable[[float], None], mn: float, rn: float) -> None:
|
|
692
|
+
super().__init__()
|
|
693
|
+
self.callback = callback
|
|
694
|
+
self.mn = mn
|
|
695
|
+
self.rn = rn
|
|
696
|
+
|
|
697
|
+
def onProgressChanged(self, seekBar: Any, progress: int, fromUser: bool) -> None:
|
|
698
|
+
if fromUser:
|
|
699
|
+
self.callback(self.mn + (progress / 1000.0) * self.rn)
|
|
700
|
+
|
|
701
|
+
def onStartTrackingTouch(self, seekBar: Any) -> None:
|
|
702
|
+
pass
|
|
703
|
+
|
|
704
|
+
def onStopTrackingTouch(self, seekBar: Any) -> None:
|
|
705
|
+
pass
|
|
706
|
+
|
|
707
|
+
sb.setOnSeekBarChangeListener(SeekProxy(cb, min_val, rng))
|
|
708
|
+
|
|
709
|
+
# ---- Pressable (FrameLayout with click listener) --------------------
|
|
710
|
+
class AndroidPressableHandler(ViewHandler):
|
|
711
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
712
|
+
fl = jclass("android.widget.FrameLayout")(_ctx())
|
|
713
|
+
fl.setClickable(True)
|
|
714
|
+
self._apply(fl, props)
|
|
715
|
+
return fl
|
|
716
|
+
|
|
717
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
718
|
+
self._apply(native_view, changed)
|
|
719
|
+
|
|
720
|
+
def _apply(self, fl: Any, props: Dict[str, Any]) -> None:
|
|
721
|
+
if "on_press" in props and props["on_press"] is not None:
|
|
722
|
+
cb = props["on_press"]
|
|
723
|
+
|
|
724
|
+
class PressProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)):
|
|
725
|
+
def __init__(self, callback: Callable[[], None]) -> None:
|
|
726
|
+
super().__init__()
|
|
727
|
+
self.callback = callback
|
|
728
|
+
|
|
729
|
+
def onClick(self, view: Any) -> None:
|
|
730
|
+
self.callback()
|
|
731
|
+
|
|
732
|
+
fl.setOnClickListener(PressProxy(cb))
|
|
733
|
+
if "on_long_press" in props and props["on_long_press"] is not None:
|
|
734
|
+
cb = props["on_long_press"]
|
|
735
|
+
|
|
736
|
+
class LongPressProxy(dynamic_proxy(jclass("android.view.View").OnLongClickListener)):
|
|
737
|
+
def __init__(self, callback: Callable[[], None]) -> None:
|
|
738
|
+
super().__init__()
|
|
739
|
+
self.callback = callback
|
|
740
|
+
|
|
741
|
+
def onLongClick(self, view: Any) -> bool:
|
|
742
|
+
self.callback()
|
|
743
|
+
return True
|
|
744
|
+
|
|
745
|
+
fl.setOnLongClickListener(LongPressProxy(cb))
|
|
746
|
+
|
|
747
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
748
|
+
parent.addView(child)
|
|
749
|
+
|
|
750
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
751
|
+
parent.removeView(child)
|
|
752
|
+
|
|
753
|
+
registry.register("Text", AndroidTextHandler())
|
|
754
|
+
registry.register("Button", AndroidButtonHandler())
|
|
755
|
+
registry.register("Column", AndroidColumnHandler())
|
|
756
|
+
registry.register("Row", AndroidRowHandler())
|
|
757
|
+
registry.register("ScrollView", AndroidScrollViewHandler())
|
|
758
|
+
registry.register("TextInput", AndroidTextInputHandler())
|
|
759
|
+
registry.register("Image", AndroidImageHandler())
|
|
760
|
+
registry.register("Switch", AndroidSwitchHandler())
|
|
761
|
+
registry.register("ProgressBar", AndroidProgressBarHandler())
|
|
762
|
+
registry.register("ActivityIndicator", AndroidActivityIndicatorHandler())
|
|
763
|
+
registry.register("WebView", AndroidWebViewHandler())
|
|
764
|
+
registry.register("Spacer", AndroidSpacerHandler())
|
|
765
|
+
registry.register("View", AndroidViewHandler())
|
|
766
|
+
registry.register("SafeAreaView", AndroidSafeAreaViewHandler())
|
|
767
|
+
registry.register("Modal", AndroidModalHandler())
|
|
768
|
+
registry.register("Slider", AndroidSliderHandler())
|
|
769
|
+
registry.register("Pressable", AndroidPressableHandler())
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
773
|
+
from rubicon.objc import SEL, ObjCClass, objc_method
|
|
774
|
+
|
|
775
|
+
NSObject = ObjCClass("NSObject")
|
|
776
|
+
UIColor = ObjCClass("UIColor")
|
|
777
|
+
UIFont = ObjCClass("UIFont")
|
|
778
|
+
|
|
779
|
+
def _uicolor(color: Any) -> Any:
|
|
780
|
+
argb = parse_color_int(color)
|
|
781
|
+
if argb < 0:
|
|
782
|
+
argb += 0x100000000
|
|
783
|
+
a = ((argb >> 24) & 0xFF) / 255.0
|
|
784
|
+
r = ((argb >> 16) & 0xFF) / 255.0
|
|
785
|
+
g = ((argb >> 8) & 0xFF) / 255.0
|
|
786
|
+
b = (argb & 0xFF) / 255.0
|
|
787
|
+
return UIColor.colorWithRed_green_blue_alpha_(r, g, b, a)
|
|
788
|
+
|
|
789
|
+
def _apply_ios_layout(view: Any, props: Dict[str, Any]) -> None:
|
|
790
|
+
"""Apply common layout constraints to an iOS view."""
|
|
791
|
+
if "width" in props and props["width"] is not None:
|
|
792
|
+
try:
|
|
793
|
+
for c in list(view.constraints or []):
|
|
794
|
+
if c.firstAttribute == 7: # NSLayoutAttributeWidth
|
|
795
|
+
c.setActive_(False)
|
|
796
|
+
view.widthAnchor.constraintEqualToConstant_(float(props["width"])).setActive_(True)
|
|
797
|
+
except Exception:
|
|
798
|
+
pass
|
|
799
|
+
if "height" in props and props["height"] is not None:
|
|
800
|
+
try:
|
|
801
|
+
for c in list(view.constraints or []):
|
|
802
|
+
if c.firstAttribute == 8: # NSLayoutAttributeHeight
|
|
803
|
+
c.setActive_(False)
|
|
804
|
+
view.heightAnchor.constraintEqualToConstant_(float(props["height"])).setActive_(True)
|
|
805
|
+
except Exception:
|
|
806
|
+
pass
|
|
807
|
+
|
|
808
|
+
# ---- Text -----------------------------------------------------------
|
|
809
|
+
class IOSTextHandler(ViewHandler):
|
|
810
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
811
|
+
label = ObjCClass("UILabel").alloc().init()
|
|
812
|
+
self._apply(label, props)
|
|
813
|
+
_apply_ios_layout(label, props)
|
|
814
|
+
return label
|
|
815
|
+
|
|
816
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
817
|
+
self._apply(native_view, changed)
|
|
818
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
819
|
+
_apply_ios_layout(native_view, changed)
|
|
820
|
+
|
|
821
|
+
def _apply(self, label: Any, props: Dict[str, Any]) -> None:
|
|
822
|
+
if "text" in props:
|
|
823
|
+
label.setText_(str(props["text"]))
|
|
824
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
825
|
+
if props.get("bold"):
|
|
826
|
+
label.setFont_(UIFont.boldSystemFontOfSize_(float(props["font_size"])))
|
|
827
|
+
else:
|
|
828
|
+
label.setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
|
|
829
|
+
elif "bold" in props and props["bold"]:
|
|
830
|
+
size = label.font().pointSize() if label.font() else 17.0
|
|
831
|
+
label.setFont_(UIFont.boldSystemFontOfSize_(size))
|
|
832
|
+
if "color" in props and props["color"] is not None:
|
|
833
|
+
label.setTextColor_(_uicolor(props["color"]))
|
|
834
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
835
|
+
label.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
836
|
+
if "max_lines" in props and props["max_lines"] is not None:
|
|
837
|
+
label.setNumberOfLines_(int(props["max_lines"]))
|
|
838
|
+
if "text_align" in props:
|
|
839
|
+
mapping = {"left": 0, "center": 1, "right": 2}
|
|
840
|
+
label.setTextAlignment_(mapping.get(props["text_align"], 0))
|
|
841
|
+
|
|
842
|
+
# ---- Button ---------------------------------------------------------
|
|
843
|
+
|
|
844
|
+
_pn_btn_handler_map: dict = {}
|
|
845
|
+
|
|
846
|
+
class _PNButtonTarget(NSObject): # type: ignore[valid-type]
|
|
847
|
+
_callback: Optional[Callable[[], None]] = None
|
|
848
|
+
|
|
849
|
+
@objc_method
|
|
850
|
+
def onTap_(self, sender: object) -> None:
|
|
851
|
+
if self._callback is not None:
|
|
852
|
+
self._callback()
|
|
853
|
+
|
|
854
|
+
_pn_retained_views: list = []
|
|
855
|
+
|
|
856
|
+
class IOSButtonHandler(ViewHandler):
|
|
857
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
858
|
+
btn = ObjCClass("UIButton").alloc().init()
|
|
859
|
+
btn.retain()
|
|
860
|
+
_pn_retained_views.append(btn)
|
|
861
|
+
_ios_blue = UIColor.colorWithRed_green_blue_alpha_(0.0, 0.478, 1.0, 1.0)
|
|
862
|
+
btn.setTitleColor_forState_(_ios_blue, 0)
|
|
863
|
+
self._apply(btn, props)
|
|
864
|
+
_apply_ios_layout(btn, props)
|
|
865
|
+
return btn
|
|
866
|
+
|
|
867
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
868
|
+
self._apply(native_view, changed)
|
|
869
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
870
|
+
_apply_ios_layout(native_view, changed)
|
|
871
|
+
|
|
872
|
+
def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
|
|
873
|
+
if "title" in props:
|
|
874
|
+
btn.setTitle_forState_(str(props["title"]), 0)
|
|
875
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
876
|
+
btn.titleLabel().setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
|
|
877
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
878
|
+
btn.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
879
|
+
if "color" not in props:
|
|
880
|
+
_white = UIColor.colorWithRed_green_blue_alpha_(1.0, 1.0, 1.0, 1.0)
|
|
881
|
+
btn.setTitleColor_forState_(_white, 0)
|
|
882
|
+
if "color" in props and props["color"] is not None:
|
|
883
|
+
btn.setTitleColor_forState_(_uicolor(props["color"]), 0)
|
|
884
|
+
if "enabled" in props:
|
|
885
|
+
btn.setEnabled_(bool(props["enabled"]))
|
|
886
|
+
if "on_click" in props:
|
|
887
|
+
existing = _pn_btn_handler_map.get(id(btn))
|
|
888
|
+
if existing is not None:
|
|
889
|
+
existing._callback = props["on_click"]
|
|
890
|
+
else:
|
|
891
|
+
handler = _PNButtonTarget.new()
|
|
892
|
+
handler._callback = props["on_click"]
|
|
893
|
+
_pn_btn_handler_map[id(btn)] = handler
|
|
894
|
+
btn.addTarget_action_forControlEvents_(handler, SEL("onTap:"), 1 << 6)
|
|
895
|
+
|
|
896
|
+
# ---- Column (vertical UIStackView) ----------------------------------
|
|
897
|
+
class IOSColumnHandler(ViewHandler):
|
|
898
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
899
|
+
sv = ObjCClass("UIStackView").alloc().initWithFrame_(((0, 0), (0, 0)))
|
|
900
|
+
sv.setAxis_(1) # vertical
|
|
901
|
+
self._apply(sv, props)
|
|
902
|
+
_apply_ios_layout(sv, props)
|
|
903
|
+
return sv
|
|
904
|
+
|
|
905
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
906
|
+
self._apply(native_view, changed)
|
|
907
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
908
|
+
_apply_ios_layout(native_view, changed)
|
|
909
|
+
|
|
910
|
+
def _apply(self, sv: Any, props: Dict[str, Any]) -> None:
|
|
911
|
+
if "spacing" in props and props["spacing"]:
|
|
912
|
+
sv.setSpacing_(float(props["spacing"]))
|
|
913
|
+
if "alignment" in props and props["alignment"]:
|
|
914
|
+
mapping = {"fill": 0, "leading": 1, "top": 1, "center": 3, "trailing": 4, "bottom": 4}
|
|
915
|
+
sv.setAlignment_(mapping.get(props["alignment"], 0))
|
|
916
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
917
|
+
sv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
918
|
+
if "padding" in props:
|
|
919
|
+
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
920
|
+
sv.setLayoutMarginsRelativeArrangement_(True)
|
|
921
|
+
try:
|
|
922
|
+
sv.setDirectionalLayoutMargins_((top, left, bottom, right))
|
|
923
|
+
except Exception:
|
|
924
|
+
sv.setLayoutMargins_((top, left, bottom, right))
|
|
925
|
+
|
|
926
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
927
|
+
parent.addArrangedSubview_(child)
|
|
928
|
+
|
|
929
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
930
|
+
parent.removeArrangedSubview_(child)
|
|
931
|
+
child.removeFromSuperview()
|
|
932
|
+
|
|
933
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
934
|
+
parent.insertArrangedSubview_atIndex_(child, index)
|
|
935
|
+
|
|
936
|
+
# ---- Row (horizontal UIStackView) -----------------------------------
|
|
937
|
+
class IOSRowHandler(ViewHandler):
|
|
938
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
939
|
+
sv = ObjCClass("UIStackView").alloc().initWithFrame_(((0, 0), (0, 0)))
|
|
940
|
+
sv.setAxis_(0) # horizontal
|
|
941
|
+
self._apply(sv, props)
|
|
942
|
+
_apply_ios_layout(sv, props)
|
|
943
|
+
return sv
|
|
944
|
+
|
|
945
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
946
|
+
self._apply(native_view, changed)
|
|
947
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
948
|
+
_apply_ios_layout(native_view, changed)
|
|
949
|
+
|
|
950
|
+
def _apply(self, sv: Any, props: Dict[str, Any]) -> None:
|
|
951
|
+
if "spacing" in props and props["spacing"]:
|
|
952
|
+
sv.setSpacing_(float(props["spacing"]))
|
|
953
|
+
if "alignment" in props and props["alignment"]:
|
|
954
|
+
mapping = {"fill": 0, "leading": 1, "top": 1, "center": 3, "trailing": 4, "bottom": 4}
|
|
955
|
+
sv.setAlignment_(mapping.get(props["alignment"], 0))
|
|
956
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
957
|
+
sv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
958
|
+
|
|
959
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
960
|
+
parent.addArrangedSubview_(child)
|
|
961
|
+
|
|
962
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
963
|
+
parent.removeArrangedSubview_(child)
|
|
964
|
+
child.removeFromSuperview()
|
|
965
|
+
|
|
966
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
967
|
+
parent.insertArrangedSubview_atIndex_(child, index)
|
|
968
|
+
|
|
969
|
+
# ---- ScrollView -----------------------------------------------------
|
|
970
|
+
class IOSScrollViewHandler(ViewHandler):
|
|
971
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
972
|
+
sv = ObjCClass("UIScrollView").alloc().init()
|
|
973
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
974
|
+
sv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
975
|
+
_apply_ios_layout(sv, props)
|
|
976
|
+
return sv
|
|
977
|
+
|
|
978
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
979
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
980
|
+
native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
|
|
981
|
+
|
|
982
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
983
|
+
child.setTranslatesAutoresizingMaskIntoConstraints_(False)
|
|
984
|
+
parent.addSubview_(child)
|
|
985
|
+
content_guide = parent.contentLayoutGuide
|
|
986
|
+
frame_guide = parent.frameLayoutGuide
|
|
987
|
+
child.topAnchor.constraintEqualToAnchor_(content_guide.topAnchor).setActive_(True)
|
|
988
|
+
child.leadingAnchor.constraintEqualToAnchor_(content_guide.leadingAnchor).setActive_(True)
|
|
989
|
+
child.trailingAnchor.constraintEqualToAnchor_(content_guide.trailingAnchor).setActive_(True)
|
|
990
|
+
child.bottomAnchor.constraintEqualToAnchor_(content_guide.bottomAnchor).setActive_(True)
|
|
991
|
+
child.widthAnchor.constraintEqualToAnchor_(frame_guide.widthAnchor).setActive_(True)
|
|
992
|
+
|
|
993
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
994
|
+
child.removeFromSuperview()
|
|
995
|
+
|
|
996
|
+
# ---- TextInput (UITextField with on_change) -------------------------
|
|
997
|
+
_pn_tf_handler_map: dict = {}
|
|
998
|
+
|
|
999
|
+
class _PNTextFieldTarget(NSObject): # type: ignore[valid-type]
|
|
1000
|
+
_callback: Optional[Callable[[str], None]] = None
|
|
1001
|
+
|
|
1002
|
+
@objc_method
|
|
1003
|
+
def onEdit_(self, sender: object) -> None:
|
|
1004
|
+
if self._callback is not None:
|
|
1005
|
+
try:
|
|
1006
|
+
text = str(sender.text) if sender and hasattr(sender, "text") else ""
|
|
1007
|
+
self._callback(text)
|
|
1008
|
+
except Exception:
|
|
1009
|
+
pass
|
|
1010
|
+
|
|
1011
|
+
class IOSTextInputHandler(ViewHandler):
|
|
1012
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1013
|
+
tf = ObjCClass("UITextField").alloc().init()
|
|
1014
|
+
tf.setBorderStyle_(2) # RoundedRect
|
|
1015
|
+
self._apply(tf, props)
|
|
1016
|
+
_apply_ios_layout(tf, props)
|
|
1017
|
+
return tf
|
|
1018
|
+
|
|
1019
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1020
|
+
self._apply(native_view, changed)
|
|
1021
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
1022
|
+
_apply_ios_layout(native_view, changed)
|
|
1023
|
+
|
|
1024
|
+
def _apply(self, tf: Any, props: Dict[str, Any]) -> None:
|
|
1025
|
+
if "value" in props:
|
|
1026
|
+
tf.setText_(str(props["value"]))
|
|
1027
|
+
if "placeholder" in props:
|
|
1028
|
+
tf.setPlaceholder_(str(props["placeholder"]))
|
|
1029
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
1030
|
+
tf.setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
|
|
1031
|
+
if "color" in props and props["color"] is not None:
|
|
1032
|
+
tf.setTextColor_(_uicolor(props["color"]))
|
|
1033
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
1034
|
+
tf.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
1035
|
+
if "secure" in props and props["secure"]:
|
|
1036
|
+
tf.setSecureTextEntry_(True)
|
|
1037
|
+
if "on_change" in props:
|
|
1038
|
+
existing = _pn_tf_handler_map.get(id(tf))
|
|
1039
|
+
if existing is not None:
|
|
1040
|
+
existing._callback = props["on_change"]
|
|
1041
|
+
else:
|
|
1042
|
+
handler = _PNTextFieldTarget.new()
|
|
1043
|
+
handler._callback = props["on_change"]
|
|
1044
|
+
_pn_tf_handler_map[id(tf)] = handler
|
|
1045
|
+
tf.addTarget_action_forControlEvents_(handler, SEL("onEdit:"), 1 << 17)
|
|
1046
|
+
|
|
1047
|
+
# ---- Image (with URL loading) ---------------------------------------
|
|
1048
|
+
class IOSImageHandler(ViewHandler):
|
|
1049
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1050
|
+
iv = ObjCClass("UIImageView").alloc().init()
|
|
1051
|
+
self._apply(iv, props)
|
|
1052
|
+
_apply_ios_layout(iv, props)
|
|
1053
|
+
return iv
|
|
1054
|
+
|
|
1055
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1056
|
+
self._apply(native_view, changed)
|
|
1057
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
1058
|
+
_apply_ios_layout(native_view, changed)
|
|
1059
|
+
|
|
1060
|
+
def _apply(self, iv: Any, props: Dict[str, Any]) -> None:
|
|
1061
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
1062
|
+
iv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
1063
|
+
if "source" in props and props["source"]:
|
|
1064
|
+
self._load_source(iv, props["source"])
|
|
1065
|
+
if "scale_type" in props and props["scale_type"]:
|
|
1066
|
+
mapping = {"cover": 2, "contain": 1, "stretch": 0, "center": 4}
|
|
1067
|
+
iv.setContentMode_(mapping.get(props["scale_type"], 1))
|
|
1068
|
+
|
|
1069
|
+
def _load_source(self, iv: Any, source: str) -> None:
|
|
1070
|
+
try:
|
|
1071
|
+
if source.startswith(("http://", "https://")):
|
|
1072
|
+
NSURL = ObjCClass("NSURL")
|
|
1073
|
+
NSData = ObjCClass("NSData")
|
|
1074
|
+
UIImage = ObjCClass("UIImage")
|
|
1075
|
+
url = NSURL.URLWithString_(source)
|
|
1076
|
+
data = NSData.dataWithContentsOfURL_(url)
|
|
1077
|
+
if data:
|
|
1078
|
+
image = UIImage.imageWithData_(data)
|
|
1079
|
+
if image:
|
|
1080
|
+
iv.setImage_(image)
|
|
1081
|
+
else:
|
|
1082
|
+
UIImage = ObjCClass("UIImage")
|
|
1083
|
+
image = UIImage.imageNamed_(source)
|
|
1084
|
+
if image:
|
|
1085
|
+
iv.setImage_(image)
|
|
1086
|
+
except Exception:
|
|
1087
|
+
pass
|
|
1088
|
+
|
|
1089
|
+
# ---- Switch (with on_change) ----------------------------------------
|
|
1090
|
+
_pn_switch_handler_map: dict = {}
|
|
1091
|
+
|
|
1092
|
+
class _PNSwitchTarget(NSObject): # type: ignore[valid-type]
|
|
1093
|
+
_callback: Optional[Callable[[bool], None]] = None
|
|
1094
|
+
|
|
1095
|
+
@objc_method
|
|
1096
|
+
def onToggle_(self, sender: object) -> None:
|
|
1097
|
+
if self._callback is not None:
|
|
1098
|
+
try:
|
|
1099
|
+
self._callback(bool(sender.isOn()))
|
|
1100
|
+
except Exception:
|
|
1101
|
+
pass
|
|
1102
|
+
|
|
1103
|
+
class IOSSwitchHandler(ViewHandler):
|
|
1104
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1105
|
+
sw = ObjCClass("UISwitch").alloc().init()
|
|
1106
|
+
self._apply(sw, props)
|
|
1107
|
+
return sw
|
|
1108
|
+
|
|
1109
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1110
|
+
self._apply(native_view, changed)
|
|
1111
|
+
|
|
1112
|
+
def _apply(self, sw: Any, props: Dict[str, Any]) -> None:
|
|
1113
|
+
if "value" in props:
|
|
1114
|
+
sw.setOn_animated_(bool(props["value"]), False)
|
|
1115
|
+
if "on_change" in props:
|
|
1116
|
+
existing = _pn_switch_handler_map.get(id(sw))
|
|
1117
|
+
if existing is not None:
|
|
1118
|
+
existing._callback = props["on_change"]
|
|
1119
|
+
else:
|
|
1120
|
+
handler = _PNSwitchTarget.new()
|
|
1121
|
+
handler._callback = props["on_change"]
|
|
1122
|
+
_pn_switch_handler_map[id(sw)] = handler
|
|
1123
|
+
sw.addTarget_action_forControlEvents_(handler, SEL("onToggle:"), 1 << 12)
|
|
1124
|
+
|
|
1125
|
+
# ---- ProgressBar (UIProgressView) -----------------------------------
|
|
1126
|
+
class IOSProgressBarHandler(ViewHandler):
|
|
1127
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1128
|
+
pv = ObjCClass("UIProgressView").alloc().init()
|
|
1129
|
+
if "value" in props:
|
|
1130
|
+
pv.setProgress_(float(props["value"]))
|
|
1131
|
+
_apply_ios_layout(pv, props)
|
|
1132
|
+
return pv
|
|
1133
|
+
|
|
1134
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1135
|
+
if "value" in changed:
|
|
1136
|
+
native_view.setProgress_(float(changed["value"]))
|
|
1137
|
+
|
|
1138
|
+
# ---- ActivityIndicator ----------------------------------------------
|
|
1139
|
+
class IOSActivityIndicatorHandler(ViewHandler):
|
|
1140
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1141
|
+
ai = ObjCClass("UIActivityIndicatorView").alloc().init()
|
|
1142
|
+
if props.get("animating", True):
|
|
1143
|
+
ai.startAnimating()
|
|
1144
|
+
return ai
|
|
1145
|
+
|
|
1146
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1147
|
+
if "animating" in changed:
|
|
1148
|
+
if changed["animating"]:
|
|
1149
|
+
native_view.startAnimating()
|
|
1150
|
+
else:
|
|
1151
|
+
native_view.stopAnimating()
|
|
1152
|
+
|
|
1153
|
+
# ---- WebView (WKWebView) --------------------------------------------
|
|
1154
|
+
class IOSWebViewHandler(ViewHandler):
|
|
1155
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1156
|
+
wv = ObjCClass("WKWebView").alloc().init()
|
|
1157
|
+
if "url" in props and props["url"]:
|
|
1158
|
+
NSURL = ObjCClass("NSURL")
|
|
1159
|
+
NSURLRequest = ObjCClass("NSURLRequest")
|
|
1160
|
+
url_obj = NSURL.URLWithString_(str(props["url"]))
|
|
1161
|
+
wv.loadRequest_(NSURLRequest.requestWithURL_(url_obj))
|
|
1162
|
+
_apply_ios_layout(wv, props)
|
|
1163
|
+
return wv
|
|
1164
|
+
|
|
1165
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1166
|
+
if "url" in changed and changed["url"]:
|
|
1167
|
+
NSURL = ObjCClass("NSURL")
|
|
1168
|
+
NSURLRequest = ObjCClass("NSURLRequest")
|
|
1169
|
+
url_obj = NSURL.URLWithString_(str(changed["url"]))
|
|
1170
|
+
native_view.loadRequest_(NSURLRequest.requestWithURL_(url_obj))
|
|
1171
|
+
|
|
1172
|
+
# ---- Spacer ---------------------------------------------------------
|
|
1173
|
+
class IOSSpacerHandler(ViewHandler):
|
|
1174
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1175
|
+
v = ObjCClass("UIView").alloc().init()
|
|
1176
|
+
if "size" in props and props["size"] is not None:
|
|
1177
|
+
size = float(props["size"])
|
|
1178
|
+
v.setFrame_(((0, 0), (size, size)))
|
|
1179
|
+
return v
|
|
1180
|
+
|
|
1181
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1182
|
+
if "size" in changed and changed["size"] is not None:
|
|
1183
|
+
size = float(changed["size"])
|
|
1184
|
+
native_view.setFrame_(((0, 0), (size, size)))
|
|
1185
|
+
|
|
1186
|
+
# ---- View (generic UIView) -----------------------------------------
|
|
1187
|
+
class IOSViewHandler(ViewHandler):
|
|
1188
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1189
|
+
v = ObjCClass("UIView").alloc().init()
|
|
1190
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
1191
|
+
v.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
1192
|
+
_apply_ios_layout(v, props)
|
|
1193
|
+
return v
|
|
1194
|
+
|
|
1195
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1196
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
1197
|
+
native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
|
|
1198
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
1199
|
+
_apply_ios_layout(native_view, changed)
|
|
1200
|
+
|
|
1201
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
1202
|
+
parent.addSubview_(child)
|
|
1203
|
+
|
|
1204
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
1205
|
+
child.removeFromSuperview()
|
|
1206
|
+
|
|
1207
|
+
# ---- SafeAreaView ---------------------------------------------------
|
|
1208
|
+
class IOSSafeAreaViewHandler(ViewHandler):
|
|
1209
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1210
|
+
v = ObjCClass("UIView").alloc().init()
|
|
1211
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
1212
|
+
v.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
1213
|
+
return v
|
|
1214
|
+
|
|
1215
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1216
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
1217
|
+
native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
|
|
1218
|
+
|
|
1219
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
1220
|
+
parent.addSubview_(child)
|
|
1221
|
+
|
|
1222
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
1223
|
+
child.removeFromSuperview()
|
|
1224
|
+
|
|
1225
|
+
# ---- Modal ----------------------------------------------------------
|
|
1226
|
+
class IOSModalHandler(ViewHandler):
|
|
1227
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1228
|
+
v = ObjCClass("UIView").alloc().init()
|
|
1229
|
+
v.setHidden_(True)
|
|
1230
|
+
return v
|
|
1231
|
+
|
|
1232
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1233
|
+
pass
|
|
1234
|
+
|
|
1235
|
+
# ---- Slider (UISlider) ----------------------------------------------
|
|
1236
|
+
_pn_slider_handler_map: dict = {}
|
|
1237
|
+
|
|
1238
|
+
class _PNSliderTarget(NSObject): # type: ignore[valid-type]
|
|
1239
|
+
_callback: Optional[Callable[[float], None]] = None
|
|
1240
|
+
|
|
1241
|
+
@objc_method
|
|
1242
|
+
def onSlide_(self, sender: object) -> None:
|
|
1243
|
+
if self._callback is not None:
|
|
1244
|
+
try:
|
|
1245
|
+
self._callback(float(sender.value))
|
|
1246
|
+
except Exception:
|
|
1247
|
+
pass
|
|
1248
|
+
|
|
1249
|
+
class IOSSliderHandler(ViewHandler):
|
|
1250
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1251
|
+
sl = ObjCClass("UISlider").alloc().init()
|
|
1252
|
+
self._apply(sl, props)
|
|
1253
|
+
_apply_ios_layout(sl, props)
|
|
1254
|
+
return sl
|
|
1255
|
+
|
|
1256
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1257
|
+
self._apply(native_view, changed)
|
|
1258
|
+
|
|
1259
|
+
def _apply(self, sl: Any, props: Dict[str, Any]) -> None:
|
|
1260
|
+
if "min_value" in props:
|
|
1261
|
+
sl.setMinimumValue_(float(props["min_value"]))
|
|
1262
|
+
if "max_value" in props:
|
|
1263
|
+
sl.setMaximumValue_(float(props["max_value"]))
|
|
1264
|
+
if "value" in props:
|
|
1265
|
+
sl.setValue_(float(props["value"]))
|
|
1266
|
+
if "on_change" in props:
|
|
1267
|
+
existing = _pn_slider_handler_map.get(id(sl))
|
|
1268
|
+
if existing is not None:
|
|
1269
|
+
existing._callback = props["on_change"]
|
|
1270
|
+
else:
|
|
1271
|
+
handler = _PNSliderTarget.new()
|
|
1272
|
+
handler._callback = props["on_change"]
|
|
1273
|
+
_pn_slider_handler_map[id(sl)] = handler
|
|
1274
|
+
sl.addTarget_action_forControlEvents_(handler, SEL("onSlide:"), 1 << 12)
|
|
1275
|
+
|
|
1276
|
+
# ---- Pressable (UIView with tap gesture) ----------------------------
|
|
1277
|
+
class IOSPressableHandler(ViewHandler):
|
|
1278
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1279
|
+
v = ObjCClass("UIView").alloc().init()
|
|
1280
|
+
v.setUserInteractionEnabled_(True)
|
|
1281
|
+
return v
|
|
1282
|
+
|
|
1283
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1284
|
+
pass
|
|
1285
|
+
|
|
1286
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
1287
|
+
parent.addSubview_(child)
|
|
1288
|
+
|
|
1289
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
1290
|
+
child.removeFromSuperview()
|
|
1291
|
+
|
|
1292
|
+
registry.register("Text", IOSTextHandler())
|
|
1293
|
+
registry.register("Button", IOSButtonHandler())
|
|
1294
|
+
registry.register("Column", IOSColumnHandler())
|
|
1295
|
+
registry.register("Row", IOSRowHandler())
|
|
1296
|
+
registry.register("ScrollView", IOSScrollViewHandler())
|
|
1297
|
+
registry.register("TextInput", IOSTextInputHandler())
|
|
1298
|
+
registry.register("Image", IOSImageHandler())
|
|
1299
|
+
registry.register("Switch", IOSSwitchHandler())
|
|
1300
|
+
registry.register("ProgressBar", IOSProgressBarHandler())
|
|
1301
|
+
registry.register("ActivityIndicator", IOSActivityIndicatorHandler())
|
|
1302
|
+
registry.register("WebView", IOSWebViewHandler())
|
|
1303
|
+
registry.register("Spacer", IOSSpacerHandler())
|
|
1304
|
+
registry.register("View", IOSViewHandler())
|
|
1305
|
+
registry.register("SafeAreaView", IOSSafeAreaViewHandler())
|
|
1306
|
+
registry.register("Modal", IOSModalHandler())
|
|
1307
|
+
registry.register("Slider", IOSSliderHandler())
|
|
1308
|
+
registry.register("Pressable", IOSPressableHandler())
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
# ======================================================================
|
|
1312
|
+
# Factory
|
|
1313
|
+
# ======================================================================
|
|
1314
|
+
|
|
1315
|
+
_registry: Optional[NativeViewRegistry] = None
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def get_registry() -> NativeViewRegistry:
|
|
1319
|
+
"""Return the singleton registry, lazily creating platform handlers."""
|
|
1320
|
+
global _registry
|
|
1321
|
+
if _registry is not None:
|
|
1322
|
+
return _registry
|
|
1323
|
+
_registry = NativeViewRegistry()
|
|
1324
|
+
if IS_ANDROID:
|
|
1325
|
+
_register_android_handlers(_registry)
|
|
1326
|
+
else:
|
|
1327
|
+
_register_ios_handlers(_registry)
|
|
1328
|
+
return _registry
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def set_registry(registry: NativeViewRegistry) -> None:
|
|
1332
|
+
"""Inject a custom or mock registry (primarily for testing)."""
|
|
1333
|
+
global _registry
|
|
1334
|
+
_registry = registry
|