pythonnative 0.23.0__py3-none-any.whl → 0.25.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 +44 -53
- pythonnative/animated.py +3 -3
- pythonnative/cli/pn.py +3 -3
- pythonnative/components.py +309 -513
- pythonnative/diagnostics.py +214 -0
- pythonnative/element.py +5 -2
- pythonnative/events.py +13 -8
- pythonnative/hooks.py +761 -114
- pythonnative/hot_reload.py +9 -1
- pythonnative/native_modules/notifications.py +36 -4
- pythonnative/native_modules/permissions.py +24 -1
- pythonnative/native_views/android.py +85 -3
- pythonnative/native_views/desktop.py +38 -1
- pythonnative/native_views/ios.py +252 -7
- pythonnative/navigation.py +41 -17
- pythonnative/preview.py +43 -7
- pythonnative/reconciler.py +1360 -457
- pythonnative/runtime.py +407 -215
- pythonnative/screen.py +377 -34
- pythonnative/storage.py +5 -5
- pythonnative/style.py +83 -0
- pythonnative/suspense.py +417 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/METADATA +6 -4
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/RECORD +29 -27
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/top_level.txt +0 -0
pythonnative/hot_reload.py
CHANGED
|
@@ -576,7 +576,10 @@ class ModuleReloader:
|
|
|
576
576
|
if getattr(vnode, "element", None) is not None:
|
|
577
577
|
rewrite_element_tree(vnode.element)
|
|
578
578
|
rendered = getattr(vnode, "_rendered", None)
|
|
579
|
-
if rendered
|
|
579
|
+
if isinstance(rendered, (list, tuple)):
|
|
580
|
+
for rendered_el in rendered:
|
|
581
|
+
rewrite_element_tree(rendered_el)
|
|
582
|
+
elif rendered is not None:
|
|
580
583
|
rewrite_element_tree(rendered)
|
|
581
584
|
for child in getattr(vnode, "children", []) or []:
|
|
582
585
|
visit(child)
|
|
@@ -598,6 +601,11 @@ class ModuleReloader:
|
|
|
598
601
|
if not replacement_map:
|
|
599
602
|
return False
|
|
600
603
|
rewrites = ModuleReloader.swap_components_in_tree(reconciler, replacement_map)
|
|
604
|
+
if rewrites > 0 and hasattr(reconciler, "reset_hook_signatures"):
|
|
605
|
+
# New component bodies may legitimately call a different
|
|
606
|
+
# hook sequence; forget the recorded signatures so the
|
|
607
|
+
# dev-mode order guard doesn't flag the refresh itself.
|
|
608
|
+
reconciler.reset_hook_signatures()
|
|
601
609
|
return rewrites > 0
|
|
602
610
|
|
|
603
611
|
@staticmethod
|
|
@@ -28,11 +28,16 @@ Example:
|
|
|
28
28
|
from __future__ import annotations
|
|
29
29
|
|
|
30
30
|
import asyncio
|
|
31
|
-
from typing import Any
|
|
31
|
+
from typing import Any, Dict
|
|
32
32
|
|
|
33
33
|
from ..runtime import resolve_future
|
|
34
34
|
from ..utils import IS_ANDROID
|
|
35
35
|
|
|
36
|
+
# Delayed Android deliveries in flight, keyed by identifier, so
|
|
37
|
+
# ``cancel`` can abort a notification that hasn't posted yet. (iOS
|
|
38
|
+
# delivers delays natively through UNTimeIntervalNotificationTrigger.)
|
|
39
|
+
_android_delayed: Dict[str, "asyncio.Task[Any]"] = {}
|
|
40
|
+
|
|
36
41
|
|
|
37
42
|
class Notifications:
|
|
38
43
|
"""Local notification interface."""
|
|
@@ -82,7 +87,13 @@ class Notifications:
|
|
|
82
87
|
"""
|
|
83
88
|
del options
|
|
84
89
|
if IS_ANDROID:
|
|
85
|
-
|
|
90
|
+
if delay_seconds > 0:
|
|
91
|
+
_cancel_android_delayed(identifier)
|
|
92
|
+
_android_delayed[identifier] = asyncio.ensure_future(
|
|
93
|
+
_android_schedule_later(title, body, delay_seconds, identifier)
|
|
94
|
+
)
|
|
95
|
+
return True
|
|
96
|
+
return await asyncio.to_thread(_android_schedule, title, body, identifier)
|
|
86
97
|
return await asyncio.to_thread(_ios_schedule, title, body, delay_seconds, identifier)
|
|
87
98
|
|
|
88
99
|
@staticmethod
|
|
@@ -94,6 +105,7 @@ class Notifications:
|
|
|
94
105
|
[`schedule`][pythonnative.native_modules.notifications.Notifications.schedule].
|
|
95
106
|
"""
|
|
96
107
|
if IS_ANDROID:
|
|
108
|
+
_cancel_android_delayed(identifier)
|
|
97
109
|
await asyncio.to_thread(_android_cancel, identifier)
|
|
98
110
|
return
|
|
99
111
|
await asyncio.to_thread(_ios_cancel, identifier)
|
|
@@ -104,8 +116,28 @@ class Notifications:
|
|
|
104
116
|
# ======================================================================
|
|
105
117
|
|
|
106
118
|
|
|
107
|
-
def
|
|
108
|
-
|
|
119
|
+
def _cancel_android_delayed(identifier: str) -> None:
|
|
120
|
+
pending = _android_delayed.pop(identifier, None)
|
|
121
|
+
if pending is not None and not pending.done():
|
|
122
|
+
pending.cancel()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def _android_schedule_later(title: str, body: str, delay_seconds: float, identifier: str) -> None:
|
|
126
|
+
"""Post an Android notification after ``delay_seconds`` on the framework loop.
|
|
127
|
+
|
|
128
|
+
The delay lives in the app process (matching the semantics of
|
|
129
|
+
``UNTimeIntervalNotificationTrigger`` closely enough for in-app
|
|
130
|
+
reminders); notifications whose delay outlives the process need
|
|
131
|
+
``AlarmManager``, which is out of scope for the built-in module.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
await asyncio.sleep(delay_seconds)
|
|
135
|
+
await asyncio.to_thread(_android_schedule, title, body, identifier)
|
|
136
|
+
finally:
|
|
137
|
+
_android_delayed.pop(identifier, None)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _android_schedule(title: str, body: str, identifier: str) -> bool:
|
|
109
141
|
try:
|
|
110
142
|
from java import jclass
|
|
111
143
|
|
|
@@ -193,8 +193,31 @@ def _ios_request(permission: str, on_done: Callable[[PermissionStatus], None]) -
|
|
|
193
193
|
|
|
194
194
|
AVCaptureDevice.requestAccessForMediaType_completionHandler_(media, Block(_granted, None, bool))
|
|
195
195
|
return
|
|
196
|
+
|
|
197
|
+
if permission == "photos":
|
|
198
|
+
PHPhotoLibrary = ObjCClass("PHPhotoLibrary")
|
|
199
|
+
|
|
200
|
+
def _photos_done(status: int) -> None:
|
|
201
|
+
# 0 notDetermined, 1 restricted, 2 denied, 3 authorized,
|
|
202
|
+
# 4 limited (counts as granted).
|
|
203
|
+
on_done({0: UNDETERMINED, 1: BLOCKED, 2: DENIED, 3: GRANTED, 4: GRANTED}.get(int(status), UNDETERMINED))
|
|
204
|
+
|
|
205
|
+
PHPhotoLibrary.requestAuthorization_(Block(_photos_done, None, int))
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if permission == "notifications":
|
|
209
|
+
center = ObjCClass("UNUserNotificationCenter").currentNotificationCenter()
|
|
210
|
+
|
|
211
|
+
def _notif_done(granted: bool, _error: Any) -> None:
|
|
212
|
+
on_done(GRANTED if granted else DENIED)
|
|
213
|
+
|
|
214
|
+
# 0x07 = badge | sound | alert.
|
|
215
|
+
center.requestAuthorizationWithOptions_completionHandler_(0x07, Block(_notif_done, None, bool, object))
|
|
216
|
+
return
|
|
196
217
|
except Exception:
|
|
197
218
|
pass
|
|
198
219
|
# Fall back to reporting the current status for permissions whose
|
|
199
|
-
# request flow isn't wired up natively
|
|
220
|
+
# request flow isn't wired up natively (location needs a
|
|
221
|
+
# CLLocationManager delegate; use the Location module's own
|
|
222
|
+
# request path for that).
|
|
200
223
|
on_done(_ios_check(permission))
|
|
@@ -612,11 +612,16 @@ def _make_end_listener(anim_id: int) -> Any:
|
|
|
612
612
|
AnimatorListener = jclass("android.animation.Animator$AnimatorListener")
|
|
613
613
|
|
|
614
614
|
class _EndProxy(dynamic_proxy(AnimatorListener)):
|
|
615
|
+
# ``onAnimationStart`` / ``onAnimationEnd`` accept an optional
|
|
616
|
+
# ``isReverse``: API 26 added default two-arg overloads to
|
|
617
|
+
# ``AnimatorListener``, and ``ValueAnimator`` invokes those, so
|
|
618
|
+
# the java.lang.reflect.Proxy forwards two arguments here.
|
|
619
|
+
|
|
615
620
|
def __init__(self) -> None:
|
|
616
621
|
super().__init__()
|
|
617
622
|
self._cancelled = False
|
|
618
623
|
|
|
619
|
-
def onAnimationStart(self, animation: Any) -> None:
|
|
624
|
+
def onAnimationStart(self, animation: Any, isReverse: bool = False) -> None:
|
|
620
625
|
pass
|
|
621
626
|
|
|
622
627
|
def onAnimationRepeat(self, animation: Any) -> None:
|
|
@@ -625,7 +630,7 @@ def _make_end_listener(anim_id: int) -> Any:
|
|
|
625
630
|
def onAnimationCancel(self, animation: Any) -> None:
|
|
626
631
|
self._cancelled = True
|
|
627
632
|
|
|
628
|
-
def onAnimationEnd(self, animation: Any) -> None:
|
|
633
|
+
def onAnimationEnd(self, animation: Any, isReverse: bool = False) -> None:
|
|
629
634
|
entry = _native_anims.pop(anim_id, None)
|
|
630
635
|
if entry is None:
|
|
631
636
|
return
|
|
@@ -1073,7 +1078,7 @@ class ButtonHandler(AndroidViewHandler):
|
|
|
1073
1078
|
|
|
1074
1079
|
class ClickProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)):
|
|
1075
1080
|
def onClick(self, view: Any) -> None:
|
|
1076
|
-
_fire(view, "
|
|
1081
|
+
_fire(view, "on_press")
|
|
1077
1082
|
|
|
1078
1083
|
btn.setOnClickListener(ClickProxy())
|
|
1079
1084
|
return btn
|
|
@@ -2094,6 +2099,81 @@ class ModalHandler(AndroidViewHandler):
|
|
|
2094
2099
|
pass
|
|
2095
2100
|
|
|
2096
2101
|
|
|
2102
|
+
# ======================================================================
|
|
2103
|
+
# Portal: floats its children over the screen in a top-level overlay
|
|
2104
|
+
# ======================================================================
|
|
2105
|
+
|
|
2106
|
+
|
|
2107
|
+
class PortalHandler(AndroidViewHandler):
|
|
2108
|
+
"""Overlay container hosting [`Portal`][pythonnative.Portal] children.
|
|
2109
|
+
|
|
2110
|
+
The portal's native view is a full-size, non-clickable
|
|
2111
|
+
``FrameLayout`` added directly to the screen's fragment container
|
|
2112
|
+
(the same parent as the screen root, so portal coordinates equal
|
|
2113
|
+
viewport coordinates). It is appended last, which stacks it above
|
|
2114
|
+
the main content; because the overlay itself never consumes
|
|
2115
|
+
touches, taps on empty areas fall through to the screen below
|
|
2116
|
+
while the portal's own children stay interactive.
|
|
2117
|
+
|
|
2118
|
+
The overlay attaches lazily on the first child insert and re-homes
|
|
2119
|
+
itself if the fragment container was rebuilt (e.g. after popping
|
|
2120
|
+
back to a previously mounted screen).
|
|
2121
|
+
"""
|
|
2122
|
+
|
|
2123
|
+
def _build(self, props: Dict[str, Any]) -> Any:
|
|
2124
|
+
overlay = jclass("android.widget.FrameLayout")(_ctx())
|
|
2125
|
+
overlay.setClickable(False)
|
|
2126
|
+
return overlay
|
|
2127
|
+
|
|
2128
|
+
def _apply(self, overlay: Any, props: Dict[str, Any], initial: bool) -> None:
|
|
2129
|
+
_apply_common_visual(overlay, props)
|
|
2130
|
+
if not initial:
|
|
2131
|
+
self._ensure_attached(overlay)
|
|
2132
|
+
|
|
2133
|
+
def _ensure_attached(self, overlay: Any) -> None:
|
|
2134
|
+
try:
|
|
2135
|
+
from ..utils import get_android_fragment_container
|
|
2136
|
+
|
|
2137
|
+
container = get_android_fragment_container()
|
|
2138
|
+
except Exception:
|
|
2139
|
+
return
|
|
2140
|
+
try:
|
|
2141
|
+
parent = overlay.getParent()
|
|
2142
|
+
if parent is not None and _java_id(parent) == _java_id(container):
|
|
2143
|
+
overlay.bringToFront()
|
|
2144
|
+
return
|
|
2145
|
+
if parent is not None:
|
|
2146
|
+
parent.removeView(overlay)
|
|
2147
|
+
LayoutParams = jclass("android.view.ViewGroup$LayoutParams")
|
|
2148
|
+
lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
2149
|
+
container.addView(overlay, lp)
|
|
2150
|
+
except Exception:
|
|
2151
|
+
pass
|
|
2152
|
+
|
|
2153
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
2154
|
+
self._ensure_attached(parent)
|
|
2155
|
+
_insert_view(parent, child, index)
|
|
2156
|
+
|
|
2157
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
2158
|
+
try:
|
|
2159
|
+
parent.removeView(child)
|
|
2160
|
+
except Exception:
|
|
2161
|
+
pass
|
|
2162
|
+
|
|
2163
|
+
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
2164
|
+
# The overlay fills its container via MATCH_PARENT layout params;
|
|
2165
|
+
# the engine frames only the portal's children.
|
|
2166
|
+
return
|
|
2167
|
+
|
|
2168
|
+
def _teardown(self, overlay: Any) -> None:
|
|
2169
|
+
try:
|
|
2170
|
+
parent = overlay.getParent()
|
|
2171
|
+
if parent is not None:
|
|
2172
|
+
parent.removeView(overlay)
|
|
2173
|
+
except Exception:
|
|
2174
|
+
pass
|
|
2175
|
+
|
|
2176
|
+
|
|
2097
2177
|
class SliderHandler(AndroidViewHandler):
|
|
2098
2178
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
2099
2179
|
sb = jclass("android.widget.SeekBar")(_ctx())
|
|
@@ -3211,6 +3291,7 @@ def register_handlers(registry: Any) -> None:
|
|
|
3211
3291
|
registry.register("Spacer", SpacerHandler())
|
|
3212
3292
|
registry.register("SafeAreaView", SafeAreaViewHandler())
|
|
3213
3293
|
registry.register("Modal", ModalHandler())
|
|
3294
|
+
registry.register("Portal", PortalHandler())
|
|
3214
3295
|
registry.register("Slider", SliderHandler())
|
|
3215
3296
|
registry.register("TabBar", TabBarHandler())
|
|
3216
3297
|
registry.register("Pressable", PressableHandler())
|
|
@@ -3238,6 +3319,7 @@ __all__ = [
|
|
|
3238
3319
|
"SpacerHandler",
|
|
3239
3320
|
"SafeAreaViewHandler",
|
|
3240
3321
|
"ModalHandler",
|
|
3322
|
+
"PortalHandler",
|
|
3241
3323
|
"SliderHandler",
|
|
3242
3324
|
"TabBarHandler",
|
|
3243
3325
|
"PressableHandler",
|
|
@@ -803,7 +803,7 @@ class TextHandler(DesktopViewHandler):
|
|
|
803
803
|
class ButtonHandler(DesktopViewHandler):
|
|
804
804
|
def build(self, props: Dict[str, Any]) -> Any:
|
|
805
805
|
button = tk.Button(_master(), highlightthickness=0, takefocus=0)
|
|
806
|
-
button.configure(command=lambda: _fire(button, "
|
|
806
|
+
button.configure(command=lambda: _fire(button, "on_press"))
|
|
807
807
|
return button
|
|
808
808
|
|
|
809
809
|
def apply(self, button: Any, props: Dict[str, Any]) -> None:
|
|
@@ -1328,6 +1328,42 @@ class ModalHandler(DesktopViewHandler):
|
|
|
1328
1328
|
return
|
|
1329
1329
|
|
|
1330
1330
|
|
|
1331
|
+
# ======================================================================
|
|
1332
|
+
# Portal
|
|
1333
|
+
# ======================================================================
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
class PortalHandler(DesktopViewHandler):
|
|
1337
|
+
"""Portal host that floats children over the whole stage.
|
|
1338
|
+
|
|
1339
|
+
Tkinter has no view that can cover the stage without also
|
|
1340
|
+
swallowing clicks, so the portal keeps its own frame unmapped and
|
|
1341
|
+
instead places each child directly against the stage (``_place``
|
|
1342
|
+
with no logical parent targets the root container). The detached
|
|
1343
|
+
layout pass produces viewport coordinates, which is exactly the
|
|
1344
|
+
stage coordinate space, and ``_place`` lifts children above the
|
|
1345
|
+
main content. Empty portal area has no widget at all, so clicks
|
|
1346
|
+
there reach whatever sits underneath.
|
|
1347
|
+
"""
|
|
1348
|
+
|
|
1349
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
1350
|
+
# No ``_pn_parent``: placement composes against the stage.
|
|
1351
|
+
child._pn_parent = None
|
|
1352
|
+
_register_child(parent, child, index)
|
|
1353
|
+
_place(child)
|
|
1354
|
+
|
|
1355
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
1356
|
+
_unregister_child(parent, child)
|
|
1357
|
+
try:
|
|
1358
|
+
child.place_forget()
|
|
1359
|
+
except Exception:
|
|
1360
|
+
pass
|
|
1361
|
+
|
|
1362
|
+
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1363
|
+
# The portal frame itself stays unmapped; only children render.
|
|
1364
|
+
return
|
|
1365
|
+
|
|
1366
|
+
|
|
1331
1367
|
# ======================================================================
|
|
1332
1368
|
# TabBar
|
|
1333
1369
|
# ======================================================================
|
|
@@ -1580,6 +1616,7 @@ def register_handlers(registry: Any) -> None:
|
|
|
1580
1616
|
registry.register("ScrollView", ScrollViewHandler())
|
|
1581
1617
|
registry.register("SafeAreaView", SafeAreaViewHandler())
|
|
1582
1618
|
registry.register("Modal", ModalHandler())
|
|
1619
|
+
registry.register("Portal", PortalHandler())
|
|
1583
1620
|
registry.register("Slider", SliderHandler())
|
|
1584
1621
|
registry.register("TabBar", TabBarHandler())
|
|
1585
1622
|
registry.register("Pressable", PressableHandler())
|
pythonnative/native_views/ios.py
CHANGED
|
@@ -1589,7 +1589,7 @@ class ButtonHandler(IOSViewHandler):
|
|
|
1589
1589
|
btn.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
1590
1590
|
btn.retain()
|
|
1591
1591
|
_pn_retained_views.append(btn)
|
|
1592
|
-
_register_control_action(btn, 1 << 6, lambda: _fire(btn, "
|
|
1592
|
+
_register_control_action(btn, 1 << 6, lambda: _fire(btn, "on_press")) # TouchUpInside
|
|
1593
1593
|
return btn
|
|
1594
1594
|
|
|
1595
1595
|
def measure_intrinsic(
|
|
@@ -3137,6 +3137,185 @@ class ModalHandler(IOSViewHandler):
|
|
|
3137
3137
|
_fire(placeholder, "on_dismiss")
|
|
3138
3138
|
|
|
3139
3139
|
|
|
3140
|
+
# ======================================================================
|
|
3141
|
+
# Portal: floats its children over the screen in a top-level overlay
|
|
3142
|
+
# ======================================================================
|
|
3143
|
+
#
|
|
3144
|
+
# The overlay is an instance of ``_PNPortalViewCTypes``, a raw libobjc
|
|
3145
|
+
# subclass of ``UIView`` overriding ``pointInside:withEvent:`` so the
|
|
3146
|
+
# overlay only claims touches that land on one of its subviews. UIKit's
|
|
3147
|
+
# default hit-testing skips a view (and its whole subtree) when
|
|
3148
|
+
# ``pointInside:`` returns NO, so taps on empty portal area fall
|
|
3149
|
+
# through to the screen content below while the portal's own children
|
|
3150
|
+
# stay fully interactive. Registered raw (not via rubicon's
|
|
3151
|
+
# ``@objc_method``) for the same arm64 FFI reliability reasons as the
|
|
3152
|
+
# other delegate classes in this module.
|
|
3153
|
+
|
|
3154
|
+
|
|
3155
|
+
class _PNCGPoint(_ct.Structure):
|
|
3156
|
+
_fields_ = [("x", _ct.c_double), ("y", _ct.c_double)]
|
|
3157
|
+
|
|
3158
|
+
|
|
3159
|
+
_PORTAL_POINT_INSIDE_TYPE = _ct.CFUNCTYPE(_ct.c_bool, _ct.c_void_p, _ct.c_void_p, _PNCGPoint, _ct.c_void_p)
|
|
3160
|
+
|
|
3161
|
+
|
|
3162
|
+
def _portal_point_inside_imp(self_ptr: int, _cmd_ptr: int, point: Any, _event_ptr: int) -> bool:
|
|
3163
|
+
"""Return whether ``point`` lands on any visible, interactive subview."""
|
|
3164
|
+
try:
|
|
3165
|
+
view = ObjCInstance(_ct.c_void_p(self_ptr))
|
|
3166
|
+
px = float(point.x)
|
|
3167
|
+
py = float(point.y)
|
|
3168
|
+
# On instances of this raw-registered class rubicon resolves
|
|
3169
|
+
# ``subviews`` as a bound method rather than a property, so it
|
|
3170
|
+
# must be called to get the NSArray.
|
|
3171
|
+
subs = view.subviews
|
|
3172
|
+
if callable(subs):
|
|
3173
|
+
subs = subs()
|
|
3174
|
+
for sub in list(subs or []):
|
|
3175
|
+
try:
|
|
3176
|
+
if bool(sub.isHidden()) or float(sub.alpha) <= 0.01:
|
|
3177
|
+
continue
|
|
3178
|
+
if not bool(sub.isUserInteractionEnabled()):
|
|
3179
|
+
continue
|
|
3180
|
+
frame = sub.frame
|
|
3181
|
+
fx = float(frame.origin.x)
|
|
3182
|
+
fy = float(frame.origin.y)
|
|
3183
|
+
fw = float(frame.size.width)
|
|
3184
|
+
fh = float(frame.size.height)
|
|
3185
|
+
if fx <= px <= fx + fw and fy <= py <= fy + fh:
|
|
3186
|
+
return True
|
|
3187
|
+
except Exception:
|
|
3188
|
+
continue
|
|
3189
|
+
return False
|
|
3190
|
+
except Exception:
|
|
3191
|
+
return False
|
|
3192
|
+
|
|
3193
|
+
|
|
3194
|
+
_portal_point_inside_imp_ref = _PORTAL_POINT_INSIDE_TYPE(_portal_point_inside_imp)
|
|
3195
|
+
|
|
3196
|
+
_UIVIEW_CLS = _get_cls(b"UIView")
|
|
3197
|
+
_PN_PORTAL_VIEW_CLS = _alloc_cls(_UIVIEW_CLS, b"_PNPortalViewCTypes", 0) if _UIVIEW_CLS else None
|
|
3198
|
+
if _PN_PORTAL_VIEW_CLS:
|
|
3199
|
+
_add_method(
|
|
3200
|
+
_PN_PORTAL_VIEW_CLS,
|
|
3201
|
+
_sel_reg(b"pointInside:withEvent:"),
|
|
3202
|
+
_ct.cast(_portal_point_inside_imp_ref, _ct.c_void_p),
|
|
3203
|
+
b"c@:{CGPoint=dd}@",
|
|
3204
|
+
)
|
|
3205
|
+
_reg_cls(_PN_PORTAL_VIEW_CLS)
|
|
3206
|
+
|
|
3207
|
+
|
|
3208
|
+
class PortalHandler(IOSViewHandler):
|
|
3209
|
+
"""Overlay container hosting [`Portal`][pythonnative.Portal] children.
|
|
3210
|
+
|
|
3211
|
+
The portal's native view is a transparent, full-size overlay added
|
|
3212
|
+
to the topmost view controller's view (the same view the screen
|
|
3213
|
+
root is attached to). Its frame mirrors the screen host's root
|
|
3214
|
+
frame (below the top safe-area inset), so portal coordinates equal
|
|
3215
|
+
viewport coordinates. Attachment happens lazily on the first child
|
|
3216
|
+
insert and re-homes itself if the host view changed (e.g. after a
|
|
3217
|
+
native modal closed).
|
|
3218
|
+
|
|
3219
|
+
When the ``_PNPortalViewCTypes`` registration is unavailable the
|
|
3220
|
+
handler degrades to a plain ``UIView`` overlay: portal children
|
|
3221
|
+
still render and receive touches, but empty portal area blocks the
|
|
3222
|
+
content below instead of passing touches through.
|
|
3223
|
+
"""
|
|
3224
|
+
|
|
3225
|
+
def _build(self, props: Dict[str, Any]) -> Any:
|
|
3226
|
+
overlay = None
|
|
3227
|
+
if _PN_PORTAL_VIEW_CLS:
|
|
3228
|
+
try:
|
|
3229
|
+
overlay = ObjCClass("_PNPortalViewCTypes").alloc().init()
|
|
3230
|
+
except Exception:
|
|
3231
|
+
overlay = None
|
|
3232
|
+
if overlay is None:
|
|
3233
|
+
overlay = ObjCClass("UIView").alloc().init()
|
|
3234
|
+
overlay.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
3235
|
+
overlay.setBackgroundColor_(_uicolor("#00000000"))
|
|
3236
|
+
overlay.retain()
|
|
3237
|
+
_pn_retained_views.append(overlay)
|
|
3238
|
+
return overlay
|
|
3239
|
+
|
|
3240
|
+
def _apply(self, overlay: Any, props: Dict[str, Any], initial: bool) -> None:
|
|
3241
|
+
_apply_common_visual(overlay, props)
|
|
3242
|
+
if not initial:
|
|
3243
|
+
self._ensure_attached(overlay)
|
|
3244
|
+
|
|
3245
|
+
def _ensure_attached(self, overlay: Any) -> None:
|
|
3246
|
+
try:
|
|
3247
|
+
UIApplication = ObjCClass("UIApplication")
|
|
3248
|
+
top = _top_view_controller_for_alert(UIApplication.sharedApplication)
|
|
3249
|
+
host = top.view if top is not None else None
|
|
3250
|
+
if host is None:
|
|
3251
|
+
return
|
|
3252
|
+
current = overlay.superview
|
|
3253
|
+
if current is not None and _objc_ptr(current) == _objc_ptr(host):
|
|
3254
|
+
# Already attached to the right host. Only reorder when
|
|
3255
|
+
# the overlay is not frontmost: reordering the subview
|
|
3256
|
+
# array during an active touch sequence (a re-render can
|
|
3257
|
+
# flush between touch-down and touch-up) can cancel the
|
|
3258
|
+
# touch and leave the portal's controls stuck
|
|
3259
|
+
# highlighted with their action never fired.
|
|
3260
|
+
try:
|
|
3261
|
+
front = host.subviews.lastObject()
|
|
3262
|
+
if front is None or _objc_ptr(front) != _objc_ptr(overlay):
|
|
3263
|
+
host.bringSubviewToFront_(overlay)
|
|
3264
|
+
except Exception:
|
|
3265
|
+
host.bringSubviewToFront_(overlay)
|
|
3266
|
+
self._sync_overlay_frame(overlay, host)
|
|
3267
|
+
return
|
|
3268
|
+
if current is not None:
|
|
3269
|
+
overlay.removeFromSuperview()
|
|
3270
|
+
host.addSubview_(overlay)
|
|
3271
|
+
self._sync_overlay_frame(overlay, host)
|
|
3272
|
+
except Exception:
|
|
3273
|
+
pass
|
|
3274
|
+
|
|
3275
|
+
@staticmethod
|
|
3276
|
+
def _sync_overlay_frame(overlay: Any, host: Any) -> None:
|
|
3277
|
+
"""Mirror the screen host's root frame (see ``_sync_root_frame``)."""
|
|
3278
|
+
try:
|
|
3279
|
+
bounds = host.bounds
|
|
3280
|
+
insets = host.safeAreaInsets
|
|
3281
|
+
top = float(insets.top)
|
|
3282
|
+
left = float(insets.left)
|
|
3283
|
+
right = float(insets.right)
|
|
3284
|
+
w = max(0.0, float(bounds.size.width) - left - right)
|
|
3285
|
+
h = max(0.0, float(bounds.size.height) - top)
|
|
3286
|
+
overlay.setFrame_(((left, top), (w, h)))
|
|
3287
|
+
overlay.setAutoresizingMask_(2 | 16) # FlexibleWidth | FlexibleHeight
|
|
3288
|
+
except Exception:
|
|
3289
|
+
pass
|
|
3290
|
+
|
|
3291
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
3292
|
+
self._ensure_attached(parent)
|
|
3293
|
+
super().insert_child(parent, child, index)
|
|
3294
|
+
|
|
3295
|
+
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
3296
|
+
# The overlay frames itself against the host view; the engine
|
|
3297
|
+
# frames only the portal's children.
|
|
3298
|
+
return
|
|
3299
|
+
|
|
3300
|
+
def measure_intrinsic(
|
|
3301
|
+
self,
|
|
3302
|
+
native_view: Any,
|
|
3303
|
+
max_width: float,
|
|
3304
|
+
max_height: float,
|
|
3305
|
+
) -> Tuple[float, float]:
|
|
3306
|
+
return (0.0, 0.0)
|
|
3307
|
+
|
|
3308
|
+
def _teardown(self, overlay: Any) -> None:
|
|
3309
|
+
try:
|
|
3310
|
+
_pn_retained_views.remove(overlay)
|
|
3311
|
+
except ValueError:
|
|
3312
|
+
pass
|
|
3313
|
+
try:
|
|
3314
|
+
overlay.release()
|
|
3315
|
+
except Exception:
|
|
3316
|
+
pass
|
|
3317
|
+
|
|
3318
|
+
|
|
3140
3319
|
# ======================================================================
|
|
3141
3320
|
# StatusBar: global side effect, no view in the tree
|
|
3142
3321
|
# ======================================================================
|
|
@@ -3972,6 +4151,28 @@ def _table_row_height(info: dict, row: int) -> float:
|
|
|
3972
4151
|
return float(info.get("row_height", 44.0))
|
|
3973
4152
|
|
|
3974
4153
|
|
|
4154
|
+
def _table_row_start(info: dict, row: int) -> float:
|
|
4155
|
+
"""Content-coordinate y where ``row`` begins."""
|
|
4156
|
+
starts = info.get("row_starts")
|
|
4157
|
+
if starts is not None and 0 <= row < len(starts):
|
|
4158
|
+
return float(starts[row])
|
|
4159
|
+
return float(info.get("row_height", 44.0)) * row
|
|
4160
|
+
|
|
4161
|
+
|
|
4162
|
+
def _table_set_heights(info: dict, heights: Optional[List[float]]) -> None:
|
|
4163
|
+
"""Store per-row heights plus their prefix sums (for row starts)."""
|
|
4164
|
+
info["row_heights"] = heights
|
|
4165
|
+
if not heights:
|
|
4166
|
+
info["row_starts"] = None
|
|
4167
|
+
return
|
|
4168
|
+
starts = [0.0] * len(heights)
|
|
4169
|
+
acc = 0.0
|
|
4170
|
+
for i, h in enumerate(heights):
|
|
4171
|
+
starts[i] = acc
|
|
4172
|
+
acc += float(h)
|
|
4173
|
+
info["row_starts"] = starts
|
|
4174
|
+
|
|
4175
|
+
|
|
3975
4176
|
def _table_num_sections_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int) -> int:
|
|
3976
4177
|
return 1
|
|
3977
4178
|
|
|
@@ -4041,7 +4242,37 @@ def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> in
|
|
|
4041
4242
|
except Exception:
|
|
4042
4243
|
pass
|
|
4043
4244
|
|
|
4044
|
-
|
|
4245
|
+
# Only rows in (or near) the visible content window get a
|
|
4246
|
+
# mounted subtree. UITableView's accessibility container calls
|
|
4247
|
+
# ``cellForRowAtIndexPath:`` for *every* row whenever an
|
|
4248
|
+
# accessibility client (VoiceOver, XCUITest / UI-test drivers)
|
|
4249
|
+
# snapshots the hierarchy, no matter the estimated heights.
|
|
4250
|
+
# Mounting those would defeat virtualization -- a 10k-row list
|
|
4251
|
+
# would mount 10k Python subtrees on the first snapshot -- and
|
|
4252
|
+
# the never-displayed cells would sit stacked at the table
|
|
4253
|
+
# origin where tests read them as visible. Off-window requests
|
|
4254
|
+
# get an empty accessibility-hidden placeholder; when the row
|
|
4255
|
+
# really scrolls on screen UIKit asks again and the content
|
|
4256
|
+
# window has moved, so the real subtree mounts.
|
|
4257
|
+
try:
|
|
4258
|
+
offset_y = float(tv.contentOffset.y)
|
|
4259
|
+
except Exception:
|
|
4260
|
+
offset_y = 0.0
|
|
4261
|
+
try:
|
|
4262
|
+
viewport_h = float(tv.bounds.size.height)
|
|
4263
|
+
except Exception:
|
|
4264
|
+
viewport_h = 0.0
|
|
4265
|
+
row_start = _table_row_start(info, row) if info else 0.0
|
|
4266
|
+
margin = viewport_h if viewport_h > 0 else 800.0
|
|
4267
|
+
in_window = (row_start + row_h >= offset_y - margin) and (
|
|
4268
|
+
row_start <= offset_y + max(viewport_h, row_h) + margin
|
|
4269
|
+
)
|
|
4270
|
+
try:
|
|
4271
|
+
cell.setAccessibilityElementsHidden_(not in_window)
|
|
4272
|
+
except Exception:
|
|
4273
|
+
pass
|
|
4274
|
+
|
|
4275
|
+
if in_window and info is not None:
|
|
4045
4276
|
render_row = info.get("render_row")
|
|
4046
4277
|
pool = info.get("pool")
|
|
4047
4278
|
if render_row is not None and pool is not None:
|
|
@@ -4195,7 +4426,19 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4195
4426
|
tv.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
4196
4427
|
tv.setSeparatorStyle_(0) # none by default; rows draw their own
|
|
4197
4428
|
try:
|
|
4198
|
-
|
|
4429
|
+
# A *nonzero* estimate keeps UITableView lazy: with
|
|
4430
|
+
# ``estimatedRowHeight = 0`` modern UIKit builds cells for
|
|
4431
|
+
# every row up front to size its content, which mounts a
|
|
4432
|
+
# Python subtree per row (defeating virtualization) and
|
|
4433
|
+
# leaves the never-displayed cells visible to the
|
|
4434
|
+
# accessibility tree. ``heightForRowAtIndexPath:`` still
|
|
4435
|
+
# returns exact heights for the rows that do come on screen.
|
|
4436
|
+
heights = props.get("row_heights")
|
|
4437
|
+
if heights:
|
|
4438
|
+
estimate = sum(float(h) for h in heights) / max(1, len(heights))
|
|
4439
|
+
else:
|
|
4440
|
+
estimate = float(props.get("row_height", 44.0) or 44.0)
|
|
4441
|
+
tv.setEstimatedRowHeight_(max(1.0, estimate))
|
|
4199
4442
|
except Exception:
|
|
4200
4443
|
pass
|
|
4201
4444
|
tv.retain()
|
|
@@ -4205,14 +4448,15 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4205
4448
|
if source_ptr == 0:
|
|
4206
4449
|
raise RuntimeError("[VirtualList][iOS] dataSource alloc returned NULL")
|
|
4207
4450
|
|
|
4208
|
-
|
|
4451
|
+
state = {
|
|
4209
4452
|
"count": int(props.get("count", 0) or 0),
|
|
4210
4453
|
"row_height": float(props.get("row_height", 44.0) or 44.0),
|
|
4211
|
-
"row_heights": props.get("row_heights"),
|
|
4212
4454
|
"render_row": props.get("render_row"),
|
|
4213
4455
|
"pool": RowHostPool(),
|
|
4214
4456
|
"tv": tv,
|
|
4215
4457
|
}
|
|
4458
|
+
_table_set_heights(state, props.get("row_heights"))
|
|
4459
|
+
_pn_table_state[source_ptr] = state
|
|
4216
4460
|
|
|
4217
4461
|
_objc_msgSend.restype = None
|
|
4218
4462
|
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p]
|
|
@@ -4220,7 +4464,6 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4220
4464
|
_objc_msgSend(tv_ptr, _SEL_SET_DATA_SOURCE, _ct.c_void_p(source_ptr))
|
|
4221
4465
|
_objc_msgSend(tv_ptr, _SEL_SET_DELEGATE, _ct.c_void_p(source_ptr))
|
|
4222
4466
|
|
|
4223
|
-
state = _pn_table_state[source_ptr]
|
|
4224
4467
|
state["source_ptr"] = source_ptr
|
|
4225
4468
|
return tv
|
|
4226
4469
|
|
|
@@ -4245,7 +4488,7 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4245
4488
|
info["row_height"] = float(props["row_height"])
|
|
4246
4489
|
data_changed = True
|
|
4247
4490
|
if "row_heights" in props:
|
|
4248
|
-
info
|
|
4491
|
+
_table_set_heights(info, props.get("row_heights"))
|
|
4249
4492
|
data_changed = True
|
|
4250
4493
|
if "render_row" in props:
|
|
4251
4494
|
info["render_row"] = props.get("render_row")
|
|
@@ -4350,6 +4593,7 @@ def register_handlers(registry: Any) -> None:
|
|
|
4350
4593
|
registry.register("ScrollView", ScrollViewHandler())
|
|
4351
4594
|
registry.register("SafeAreaView", SafeAreaViewHandler())
|
|
4352
4595
|
registry.register("Modal", ModalHandler())
|
|
4596
|
+
registry.register("Portal", PortalHandler())
|
|
4353
4597
|
registry.register("Slider", SliderHandler())
|
|
4354
4598
|
registry.register("TabBar", TabBarHandler())
|
|
4355
4599
|
registry.register("Pressable", PressableHandler())
|
|
@@ -4377,6 +4621,7 @@ __all__ = [
|
|
|
4377
4621
|
"SpacerHandler",
|
|
4378
4622
|
"SafeAreaViewHandler",
|
|
4379
4623
|
"ModalHandler",
|
|
4624
|
+
"PortalHandler",
|
|
4380
4625
|
"SliderHandler",
|
|
4381
4626
|
"TabBarHandler",
|
|
4382
4627
|
"PressableHandler",
|