pythonnative 0.25.0__py3-none-any.whl → 0.27.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 +1 -1
- pythonnative/animated.py +697 -62
- pythonnative/cli/pn.py +172 -12
- pythonnative/components.py +61 -7
- pythonnative/diagnostics.py +27 -0
- pythonnative/gestures.py +446 -34
- pythonnative/native_modules/app_state.py +2 -1
- pythonnative/native_modules/battery.py +3 -2
- pythonnative/native_modules/biometrics.py +2 -1
- pythonnative/native_modules/camera.py +16 -7
- pythonnative/native_modules/clipboard.py +3 -2
- pythonnative/native_modules/file_system.py +3 -2
- pythonnative/native_modules/haptics.py +7 -6
- pythonnative/native_modules/linking.py +62 -6
- pythonnative/native_modules/location.py +11 -8
- pythonnative/native_modules/net_info.py +75 -5
- pythonnative/native_modules/notifications.py +134 -15
- pythonnative/native_modules/permissions.py +3 -2
- pythonnative/native_modules/share.py +2 -1
- pythonnative/native_views/android.py +461 -137
- pythonnative/native_views/desktop.py +816 -96
- pythonnative/native_views/ios.py +750 -193
- pythonnative/project/android.py +15 -3
- pythonnative/project/builder.py +172 -70
- pythonnative/project/config.py +28 -6
- pythonnative/project/devices.py +291 -0
- pythonnative/project/doctor.py +5 -4
- pythonnative/project/ios.py +51 -77
- pythonnative/project/permissions.py +19 -1
- pythonnative/project/runtime_assets.py +78 -166
- pythonnative/reconciler.py +28 -0
- pythonnative/style.py +19 -0
- pythonnative/templates/android_template/app/build.gradle +9 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +5 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +148 -12
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNFrameLayout.kt +50 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +5 -2
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
- pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/METADATA +3 -3
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/RECORD +49 -46
- pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/top_level.txt +0 -0
pythonnative/native_views/ios.py
CHANGED
|
@@ -14,20 +14,38 @@ is forwarded through
|
|
|
14
14
|
view's reconciler-assigned tag.
|
|
15
15
|
|
|
16
16
|
**Layout** is owned by the pure-Python flex engine in
|
|
17
|
-
`pythonnative.layout`: container handlers create
|
|
18
|
-
|
|
17
|
+
`pythonnative.layout`: container handlers create hit-test-aware
|
|
18
|
+
``UIView`` subclasses (see ``_PNInteractiveViewCTypes``), the engine
|
|
19
|
+
computes per-child frames in points, and
|
|
19
20
|
[`set_frame`][pythonnative.native_views.ios.IOSViewHandler.set_frame]
|
|
20
21
|
applies those frames via UIKit's classic ``frame`` property (with Auto
|
|
21
22
|
Layout disabled).
|
|
22
23
|
|
|
23
24
|
**Gestures** attach real ``UIGestureRecognizer`` instances (pan, pinch,
|
|
24
|
-
rotation, tap, long-press, swipe) configured from the
|
|
25
|
-
gesture specs, so recognition runs fully natively.
|
|
25
|
+
rotation, tap, long-press, swipe, fling) configured from the
|
|
26
|
+
serialized gesture specs, so recognition runs fully natively. The
|
|
27
|
+
composition metadata emitted by
|
|
28
|
+
[`serialize_gestures`][pythonnative.gestures.serialize_gestures] is
|
|
29
|
+
honored natively too: the shared delegate answers
|
|
30
|
+
``shouldRecognizeSimultaneously...`` from each spec's
|
|
31
|
+
``"simultaneous"`` set, and ``"wait_for"`` relationships map onto
|
|
32
|
+
``requireGestureRecognizerToFail:``.
|
|
33
|
+
|
|
34
|
+
**Interaction props**: ``pointer_events`` and ``hit_slop`` are
|
|
35
|
+
implemented by overriding ``pointInside:withEvent:`` and
|
|
36
|
+
``hitTest:withEvent:`` on the container subclass; ``z_index`` maps to
|
|
37
|
+
``layer.zPosition``. Per-corner radii (``border_top_left_radius`` and
|
|
38
|
+
friends) use ``layer.maskedCorners`` when one radius is shared and a
|
|
39
|
+
``CAShapeLayer`` mask when radii differ.
|
|
26
40
|
|
|
27
41
|
**Animations**: ``timing`` and ``spring`` specs are driven by UIKit
|
|
28
42
|
block animations with completion callbacks reported back through
|
|
29
43
|
``pythonnative.animated.native_animation_completed``; ``decay`` falls
|
|
30
|
-
back to the Python ticker.
|
|
44
|
+
back to the Python ticker. Python-driven frames arriving through
|
|
45
|
+
``set_animated_property`` accept floats for opacity and transform
|
|
46
|
+
channels, numeric degrees for ``rotate``, and ``"#RRGGBB"`` /
|
|
47
|
+
``"#AARRGGBB"`` strings for ``background_color`` and ``color``;
|
|
48
|
+
unknown properties are ignored.
|
|
31
49
|
|
|
32
50
|
This module is only imported on iOS at runtime. Desktop tests inject a
|
|
33
51
|
mock registry via
|
|
@@ -42,6 +60,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
42
60
|
|
|
43
61
|
from rubicon.objc import SEL, ObjCClass, ObjCInstance, objc_method
|
|
44
62
|
|
|
63
|
+
from .. import diagnostics
|
|
45
64
|
from ..events import dispatch_event, event_names
|
|
46
65
|
from . import _tripwire_log
|
|
47
66
|
from .base import ViewHandler, _safe_max, parse_color_int
|
|
@@ -80,7 +99,7 @@ try:
|
|
|
80
99
|
_UIView = ObjCClass("UIView")
|
|
81
100
|
_UIView.declare_property("superview")
|
|
82
101
|
except Exception:
|
|
83
|
-
|
|
102
|
+
diagnostics.swallowed("ios.<module>")
|
|
84
103
|
|
|
85
104
|
|
|
86
105
|
def _objc_ptr(obj: Any) -> Optional[int]:
|
|
@@ -204,6 +223,10 @@ _add_method = _libobjc.class_addMethod
|
|
|
204
223
|
_add_method.restype = _ct.c_bool
|
|
205
224
|
_add_method.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_char_p]
|
|
206
225
|
|
|
226
|
+
_get_method_imp = _libobjc.class_getMethodImplementation
|
|
227
|
+
_get_method_imp.restype = _ct.c_void_p
|
|
228
|
+
_get_method_imp.argtypes = [_ct.c_void_p, _ct.c_void_p]
|
|
229
|
+
|
|
207
230
|
_objc_msgSend = _libobjc.objc_msgSend
|
|
208
231
|
|
|
209
232
|
_SEL_ALLOC = _sel_reg(b"alloc")
|
|
@@ -225,6 +248,11 @@ _SEL_TEXT_VIEW_DID_END = _sel_reg(b"textViewDidEndEditing:")
|
|
|
225
248
|
_SEL_TEXT_VIEW_DID_CHANGE = _sel_reg(b"textViewDidChange:")
|
|
226
249
|
|
|
227
250
|
_NS_OBJECT_CLS = _get_cls(b"NSObject")
|
|
251
|
+
_UIVIEW_CLS = _get_cls(b"UIView")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class _PNCGPoint(_ct.Structure):
|
|
255
|
+
_fields_ = [("x", _ct.c_double), ("y", _ct.c_double)]
|
|
228
256
|
|
|
229
257
|
|
|
230
258
|
# ======================================================================
|
|
@@ -267,17 +295,17 @@ def _apply_border(layer: Any, props: Dict[str, Any]) -> None:
|
|
|
267
295
|
# asks for corners (matches iOS UIKit common practice).
|
|
268
296
|
layer.setMasksToBounds_(True)
|
|
269
297
|
except Exception:
|
|
270
|
-
|
|
298
|
+
diagnostics.swallowed("ios._apply_border")
|
|
271
299
|
if "border_width" in props and props["border_width"] is not None:
|
|
272
300
|
try:
|
|
273
301
|
layer.setBorderWidth_(float(props["border_width"]))
|
|
274
302
|
except Exception:
|
|
275
|
-
|
|
303
|
+
diagnostics.swallowed("ios._apply_border")
|
|
276
304
|
if "border_color" in props and props["border_color"] is not None:
|
|
277
305
|
try:
|
|
278
306
|
layer.setBorderColor_(_cgcolor(props["border_color"]))
|
|
279
307
|
except Exception:
|
|
280
|
-
|
|
308
|
+
diagnostics.swallowed("ios._apply_border")
|
|
281
309
|
|
|
282
310
|
|
|
283
311
|
def _apply_view_border(view: Any, props: Dict[str, Any]) -> None:
|
|
@@ -294,13 +322,13 @@ def _apply_view_border(view: Any, props: Dict[str, Any]) -> None:
|
|
|
294
322
|
if width > 0.0 and height > 0.0:
|
|
295
323
|
radius = min(requested, min(width, height) / 2.0)
|
|
296
324
|
except Exception:
|
|
297
|
-
|
|
325
|
+
diagnostics.swallowed("ios._apply_view_border")
|
|
298
326
|
border_props = dict(props)
|
|
299
327
|
border_props["border_radius"] = radius
|
|
300
328
|
_apply_border(view.layer, border_props)
|
|
301
329
|
return
|
|
302
330
|
except Exception:
|
|
303
|
-
|
|
331
|
+
diagnostics.swallowed("ios._apply_view_border")
|
|
304
332
|
_apply_border(view.layer, props)
|
|
305
333
|
|
|
306
334
|
|
|
@@ -344,7 +372,7 @@ def _apply_side_borders(view: Any, props: Dict[str, Any]) -> None:
|
|
|
344
372
|
try:
|
|
345
373
|
view.layer.setBorderWidth_(0.0)
|
|
346
374
|
except Exception:
|
|
347
|
-
|
|
375
|
+
diagnostics.swallowed("ios._apply_side_borders")
|
|
348
376
|
try:
|
|
349
377
|
bounds = view.bounds
|
|
350
378
|
w = float(bounds.size.width)
|
|
@@ -352,7 +380,7 @@ def _apply_side_borders(view: Any, props: Dict[str, Any]) -> None:
|
|
|
352
380
|
if w > 0.0 and h > 0.0:
|
|
353
381
|
_update_side_border_layers(view, w, h)
|
|
354
382
|
except Exception:
|
|
355
|
-
|
|
383
|
+
diagnostics.swallowed("ios._apply_side_borders")
|
|
356
384
|
|
|
357
385
|
|
|
358
386
|
def _update_side_border_layers(view: Any, width: float, height: float) -> None:
|
|
@@ -377,7 +405,7 @@ def _update_side_border_layers(view: Any, width: float, height: float) -> None:
|
|
|
377
405
|
try:
|
|
378
406
|
layer.removeFromSuperlayer()
|
|
379
407
|
except Exception:
|
|
380
|
-
|
|
408
|
+
diagnostics.swallowed("ios._update_side_border_layers")
|
|
381
409
|
layers[i] = None
|
|
382
410
|
continue
|
|
383
411
|
try:
|
|
@@ -388,7 +416,7 @@ def _update_side_border_layers(view: Any, width: float, height: float) -> None:
|
|
|
388
416
|
layer.setBackgroundColor_(_cgcolor(entry["colors"][i]))
|
|
389
417
|
layer.setFrame_(frames[i])
|
|
390
418
|
except Exception:
|
|
391
|
-
|
|
419
|
+
diagnostics.swallowed("ios._update_side_border_layers")
|
|
392
420
|
|
|
393
421
|
|
|
394
422
|
def _clamp_view_corner_radius(view: Any, width: float, height: float) -> None:
|
|
@@ -402,7 +430,7 @@ def _clamp_view_corner_radius(view: Any, width: float, height: float) -> None:
|
|
|
402
430
|
try:
|
|
403
431
|
view.layer.setCornerRadius_(min(float(requested), max_radius))
|
|
404
432
|
except Exception:
|
|
405
|
-
|
|
433
|
+
diagnostics.swallowed("ios._clamp_view_corner_radius")
|
|
406
434
|
|
|
407
435
|
|
|
408
436
|
def _clamp_layer_corner_radius(layer: Any, width: float, height: float) -> None:
|
|
@@ -418,7 +446,140 @@ def _clamp_layer_corner_radius(layer: Any, width: float, height: float) -> None:
|
|
|
418
446
|
try:
|
|
419
447
|
layer.setCornerRadius_(min(requested, max_radius))
|
|
420
448
|
except Exception:
|
|
421
|
-
|
|
449
|
+
diagnostics.swallowed("ios._clamp_layer_corner_radius")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# ----------------------------------------------------------------------
|
|
453
|
+
# Per-corner radii
|
|
454
|
+
# ----------------------------------------------------------------------
|
|
455
|
+
#
|
|
456
|
+
# When every rounded corner shares one radius the cheap path applies:
|
|
457
|
+
# ``layer.cornerRadius`` restricted to the rounded corners through
|
|
458
|
+
# ``layer.maskedCorners``. When the radii differ, UIKit has no direct
|
|
459
|
+
# API, so the view's layer gets a ``CAShapeLayer`` mask built from a
|
|
460
|
+
# ``UIBezierPath`` with one arc per corner; the mask depends on the
|
|
461
|
+
# view's bounds, so ``set_frame`` rebuilds it on every frame change via
|
|
462
|
+
# ``_update_corner_mask``.
|
|
463
|
+
|
|
464
|
+
# Order matches ``_MASKED_CORNER_BITS``: top-left, top-right,
|
|
465
|
+
# bottom-left, bottom-right.
|
|
466
|
+
_CORNER_RADIUS_KEYS = (
|
|
467
|
+
"border_top_left_radius",
|
|
468
|
+
"border_top_right_radius",
|
|
469
|
+
"border_bottom_left_radius",
|
|
470
|
+
"border_bottom_right_radius",
|
|
471
|
+
)
|
|
472
|
+
# CACornerMask bits: kCALayerMinXMinYCorner (top-left) = 1,
|
|
473
|
+
# MaxXMinY (top-right) = 2, MinXMaxY (bottom-left) = 4,
|
|
474
|
+
# MaxXMaxY (bottom-right) = 8.
|
|
475
|
+
_MASKED_CORNER_BITS = (1, 2, 4, 8)
|
|
476
|
+
_ALL_MASKED_CORNERS = 15
|
|
477
|
+
|
|
478
|
+
# id(view) -> (tl, tr, bl, br) radii for views on the shape-mask path.
|
|
479
|
+
_pn_corner_radii_map: dict = {}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _apply_corner_radii(view: Any, merged: Dict[str, Any]) -> None:
|
|
483
|
+
"""Apply per-corner radii from the *merged* prop set.
|
|
484
|
+
|
|
485
|
+
Unspecified corners inherit the uniform ``border_radius`` (else 0),
|
|
486
|
+
matching the Android backend. With every per-corner key absent the
|
|
487
|
+
entry is dropped and the uniform path owns the corners again.
|
|
488
|
+
"""
|
|
489
|
+
if not any(merged.get(k) is not None for k in _CORNER_RADIUS_KEYS):
|
|
490
|
+
if _pn_corner_radii_map.pop(id(view), None) is not None:
|
|
491
|
+
try:
|
|
492
|
+
view.layer.setMask_(None)
|
|
493
|
+
view.layer.setMaskedCorners_(_ALL_MASKED_CORNERS)
|
|
494
|
+
except Exception:
|
|
495
|
+
diagnostics.swallowed("ios._apply_corner_radii")
|
|
496
|
+
_apply_view_border(view, merged)
|
|
497
|
+
return
|
|
498
|
+
base = float(merged.get("border_radius") or 0.0)
|
|
499
|
+
radii = tuple(
|
|
500
|
+
max(0.0, _safe_finite(merged[k], base)) if merged.get(k) is not None else max(0.0, base)
|
|
501
|
+
for k in _CORNER_RADIUS_KEYS
|
|
502
|
+
)
|
|
503
|
+
distinct = {r for r in radii if r > 0.0}
|
|
504
|
+
if len(distinct) <= 1:
|
|
505
|
+
# One shared radius (or all zero): plain cornerRadius limited
|
|
506
|
+
# to the rounded corners.
|
|
507
|
+
_pn_corner_radii_map.pop(id(view), None)
|
|
508
|
+
value = distinct.pop() if distinct else 0.0
|
|
509
|
+
mask = 0
|
|
510
|
+
for bit, r in zip(_MASKED_CORNER_BITS, radii):
|
|
511
|
+
if r > 0.0:
|
|
512
|
+
mask |= bit
|
|
513
|
+
try:
|
|
514
|
+
view.layer.setMask_(None)
|
|
515
|
+
view.layer.setCornerRadius_(value)
|
|
516
|
+
view.layer.setMaskedCorners_(mask if mask else _ALL_MASKED_CORNERS)
|
|
517
|
+
view.layer.setMasksToBounds_(True)
|
|
518
|
+
except Exception:
|
|
519
|
+
diagnostics.swallowed("ios._apply_corner_radii")
|
|
520
|
+
# Route the shared value through the frame-time pill clamp.
|
|
521
|
+
_pn_view_border_radius_map[id(view)] = value
|
|
522
|
+
return
|
|
523
|
+
# Differing radii: shape-layer mask, rebuilt at frame time.
|
|
524
|
+
_pn_view_border_radius_map.pop(id(view), None)
|
|
525
|
+
_pn_corner_radii_map[id(view)] = radii
|
|
526
|
+
try:
|
|
527
|
+
view.layer.setCornerRadius_(0.0)
|
|
528
|
+
view.layer.setMaskedCorners_(_ALL_MASKED_CORNERS)
|
|
529
|
+
except Exception:
|
|
530
|
+
diagnostics.swallowed("ios._apply_corner_radii")
|
|
531
|
+
try:
|
|
532
|
+
bounds = view.bounds
|
|
533
|
+
w = float(bounds.size.width)
|
|
534
|
+
h = float(bounds.size.height)
|
|
535
|
+
if w > 0.0 and h > 0.0:
|
|
536
|
+
_update_corner_mask(view, w, h)
|
|
537
|
+
except Exception:
|
|
538
|
+
diagnostics.swallowed("ios._apply_corner_radii")
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _update_corner_mask(view: Any, width: float, height: float) -> None:
|
|
542
|
+
"""(Re)build the CAShapeLayer corner mask for the current bounds."""
|
|
543
|
+
radii = _pn_corner_radii_map.get(id(view))
|
|
544
|
+
if radii is None or width <= 0.0 or height <= 0.0:
|
|
545
|
+
return
|
|
546
|
+
tl, tr, bl, br = radii
|
|
547
|
+
# CSS-style scaling so adjacent radii never overlap along an edge.
|
|
548
|
+
scale = 1.0
|
|
549
|
+
for pair_sum, extent in ((tl + tr, width), (bl + br, width), (tl + bl, height), (tr + br, height)):
|
|
550
|
+
if pair_sum > extent > 0.0:
|
|
551
|
+
scale = min(scale, extent / pair_sum)
|
|
552
|
+
tl, tr, bl, br = (r * scale for r in radii)
|
|
553
|
+
try:
|
|
554
|
+
path = ObjCClass("UIBezierPath").bezierPath()
|
|
555
|
+
half_pi = math.pi / 2.0
|
|
556
|
+
path.moveToPoint_((tl, 0.0))
|
|
557
|
+
path.addLineToPoint_((width - tr, 0.0))
|
|
558
|
+
if tr > 0.0:
|
|
559
|
+
path.addArcWithCenter_radius_startAngle_endAngle_clockwise_((width - tr, tr), tr, -half_pi, 0.0, True)
|
|
560
|
+
path.addLineToPoint_((width, height - br))
|
|
561
|
+
if br > 0.0:
|
|
562
|
+
path.addArcWithCenter_radius_startAngle_endAngle_clockwise_(
|
|
563
|
+
(width - br, height - br), br, 0.0, half_pi, True
|
|
564
|
+
)
|
|
565
|
+
path.addLineToPoint_((bl, height))
|
|
566
|
+
if bl > 0.0:
|
|
567
|
+
path.addArcWithCenter_radius_startAngle_endAngle_clockwise_((bl, height - bl), bl, half_pi, math.pi, True)
|
|
568
|
+
path.addLineToPoint_((0.0, tl))
|
|
569
|
+
if tl > 0.0:
|
|
570
|
+
path.addArcWithCenter_radius_startAngle_endAngle_clockwise_((tl, tl), tl, math.pi, math.pi + half_pi, True)
|
|
571
|
+
path.closePath()
|
|
572
|
+
# On raw-registered outbound wrappers rubicon sometimes resolves
|
|
573
|
+
# ``CGPath`` as a bound method rather than a property.
|
|
574
|
+
cgpath = path.CGPath
|
|
575
|
+
if callable(cgpath):
|
|
576
|
+
cgpath = cgpath()
|
|
577
|
+
shape = ObjCClass("CAShapeLayer").layer()
|
|
578
|
+
shape.setFrame_(((0.0, 0.0), (width, height)))
|
|
579
|
+
shape.setPath_(cgpath)
|
|
580
|
+
view.layer.setMask_(shape)
|
|
581
|
+
except Exception:
|
|
582
|
+
diagnostics.swallowed("ios._update_corner_mask")
|
|
422
583
|
|
|
423
584
|
|
|
424
585
|
def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
|
|
@@ -434,22 +595,22 @@ def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
|
|
|
434
595
|
layer.setMasksToBounds_(False)
|
|
435
596
|
view.setClipsToBounds_(False)
|
|
436
597
|
except Exception:
|
|
437
|
-
|
|
598
|
+
diagnostics.swallowed("ios._apply_shadow")
|
|
438
599
|
if "shadow_color" in props and props["shadow_color"] is not None:
|
|
439
600
|
try:
|
|
440
601
|
layer.setShadowColor_(_cgcolor(props["shadow_color"]))
|
|
441
602
|
except Exception:
|
|
442
|
-
|
|
603
|
+
diagnostics.swallowed("ios._apply_shadow")
|
|
443
604
|
if "shadow_opacity" in props and props["shadow_opacity"] is not None:
|
|
444
605
|
try:
|
|
445
606
|
layer.setShadowOpacity_(float(props["shadow_opacity"]))
|
|
446
607
|
except Exception:
|
|
447
|
-
|
|
608
|
+
diagnostics.swallowed("ios._apply_shadow")
|
|
448
609
|
if "shadow_radius" in props and props["shadow_radius"] is not None:
|
|
449
610
|
try:
|
|
450
611
|
layer.setShadowRadius_(float(props["shadow_radius"]))
|
|
451
612
|
except Exception:
|
|
452
|
-
|
|
613
|
+
diagnostics.swallowed("ios._apply_shadow")
|
|
453
614
|
if "shadow_offset" in props and props["shadow_offset"] is not None:
|
|
454
615
|
offset = props["shadow_offset"]
|
|
455
616
|
try:
|
|
@@ -460,7 +621,7 @@ def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
|
|
|
460
621
|
w, h = float(offset[0]), float(offset[1])
|
|
461
622
|
layer.setShadowOffset_((w, h))
|
|
462
623
|
except Exception:
|
|
463
|
-
|
|
624
|
+
diagnostics.swallowed("ios._apply_shadow")
|
|
464
625
|
|
|
465
626
|
|
|
466
627
|
def _make_transform(spec: Any) -> Any:
|
|
@@ -534,7 +695,7 @@ def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
|
|
|
534
695
|
try:
|
|
535
696
|
view.setTransform_((1.0, 0.0, 0.0, 1.0, 0.0, 0.0))
|
|
536
697
|
except Exception:
|
|
537
|
-
|
|
698
|
+
diagnostics.swallowed("ios._apply_transform")
|
|
538
699
|
return
|
|
539
700
|
try:
|
|
540
701
|
transform = _make_transform(spec)
|
|
@@ -565,7 +726,7 @@ def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
|
|
|
565
726
|
# rubicon-objc accepts the C struct as a tuple of its fields.
|
|
566
727
|
view.setTransform_((a, b, c, d, tx, ty))
|
|
567
728
|
except Exception:
|
|
568
|
-
|
|
729
|
+
diagnostics.swallowed("ios._apply_transform")
|
|
569
730
|
|
|
570
731
|
|
|
571
732
|
# UIAccessibilityTraits bitmask values (UIKit constants).
|
|
@@ -604,25 +765,25 @@ def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
|
|
|
604
765
|
try:
|
|
605
766
|
view.setIsAccessibilityElement_(bool(props["accessible"]))
|
|
606
767
|
except Exception:
|
|
607
|
-
|
|
768
|
+
diagnostics.swallowed("ios._apply_accessibility")
|
|
608
769
|
if "accessibility_label" in props:
|
|
609
770
|
v = props["accessibility_label"]
|
|
610
771
|
try:
|
|
611
772
|
view.setAccessibilityLabel_(str(v) if v is not None else "")
|
|
612
773
|
except Exception:
|
|
613
|
-
|
|
774
|
+
diagnostics.swallowed("ios._apply_accessibility")
|
|
614
775
|
if "accessibility_hint" in props:
|
|
615
776
|
v = props["accessibility_hint"]
|
|
616
777
|
try:
|
|
617
778
|
view.setAccessibilityHint_(str(v) if v is not None else "")
|
|
618
779
|
except Exception:
|
|
619
|
-
|
|
780
|
+
diagnostics.swallowed("ios._apply_accessibility")
|
|
620
781
|
if "test_id" in props:
|
|
621
782
|
v = props["test_id"]
|
|
622
783
|
try:
|
|
623
784
|
view.setAccessibilityIdentifier_(str(v) if v is not None else None)
|
|
624
785
|
except Exception:
|
|
625
|
-
|
|
786
|
+
diagnostics.swallowed("ios._apply_accessibility")
|
|
626
787
|
|
|
627
788
|
role = props.get("accessibility_role")
|
|
628
789
|
state = props.get("accessibility_state")
|
|
@@ -642,11 +803,11 @@ def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
|
|
|
642
803
|
try:
|
|
643
804
|
view.setAccessibilityValue_("expanded" if state["expanded"] else "collapsed")
|
|
644
805
|
except Exception:
|
|
645
|
-
|
|
806
|
+
diagnostics.swallowed("ios._apply_accessibility")
|
|
646
807
|
try:
|
|
647
808
|
view.setAccessibilityTraits_(traits)
|
|
648
809
|
except Exception:
|
|
649
|
-
|
|
810
|
+
diagnostics.swallowed("ios._apply_accessibility")
|
|
650
811
|
|
|
651
812
|
|
|
652
813
|
def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
|
|
@@ -657,21 +818,242 @@ def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
|
|
|
657
818
|
try:
|
|
658
819
|
view.layer.setBackgroundColor_(color.CGColor)
|
|
659
820
|
except Exception:
|
|
660
|
-
|
|
821
|
+
diagnostics.swallowed("ios._apply_common_visual")
|
|
661
822
|
if "overflow" in props:
|
|
662
823
|
view.setClipsToBounds_(props["overflow"] == "hidden")
|
|
663
824
|
if "opacity" in props and props["opacity"] is not None:
|
|
664
825
|
try:
|
|
665
826
|
view.setAlpha_(float(props["opacity"]))
|
|
666
827
|
except Exception:
|
|
667
|
-
|
|
828
|
+
diagnostics.swallowed("ios._apply_common_visual")
|
|
829
|
+
if "z_index" in props:
|
|
830
|
+
# A removed prop (None) restores the default plane.
|
|
831
|
+
try:
|
|
832
|
+
z = props["z_index"]
|
|
833
|
+
view.layer.setZPosition_(float(z) if z is not None else 0.0)
|
|
834
|
+
except Exception:
|
|
835
|
+
diagnostics.swallowed("ios._apply_common_visual")
|
|
836
|
+
if "pointer_events" in props or "hit_slop" in props:
|
|
837
|
+
_apply_interaction_props(view, props)
|
|
668
838
|
_apply_view_border(view, props)
|
|
669
839
|
_apply_side_borders(view, props)
|
|
840
|
+
# Per-corner radii read the merged prop set (an update may carry
|
|
841
|
+
# only one key); a lone border_radius change must also re-run the
|
|
842
|
+
# per-corner path when corner keys are active, or the uniform
|
|
843
|
+
# applier above would stomp the masked corners.
|
|
844
|
+
merged = _state_of(view).get("props") or props
|
|
845
|
+
if any(k in props for k in _CORNER_RADIUS_KEYS) or (
|
|
846
|
+
"border_radius" in props and any(merged.get(k) is not None for k in _CORNER_RADIUS_KEYS)
|
|
847
|
+
):
|
|
848
|
+
_apply_corner_radii(view, merged)
|
|
670
849
|
_apply_shadow(view, props)
|
|
671
850
|
_apply_transform(view, props)
|
|
672
851
|
_apply_accessibility(view, props)
|
|
673
852
|
|
|
674
853
|
|
|
854
|
+
# ======================================================================
|
|
855
|
+
# Interaction props: pointer_events / hit_slop (raw libobjc subclass)
|
|
856
|
+
# ======================================================================
|
|
857
|
+
#
|
|
858
|
+
# ``pointer_events`` and ``hit_slop`` need to change how UIKit
|
|
859
|
+
# hit-tests a view, which requires overriding ``pointInside:withEvent:``
|
|
860
|
+
# and ``hitTest:withEvent:``. Like ``_PNPortalViewCTypes``, the
|
|
861
|
+
# ``_PNInteractiveViewCTypes`` class is registered with raw libobjc
|
|
862
|
+
# (CFUNCTYPE IMPs) rather than rubicon's ``@objc_method``, for the same
|
|
863
|
+
# arm64 FFI reliability reasons documented in the module header.
|
|
864
|
+
#
|
|
865
|
+
# Container handlers (View / Row / Column, Pressable, SafeAreaView,
|
|
866
|
+
# KeyboardAvoidingView) *always* instantiate this subclass. That is the
|
|
867
|
+
# low-risk choice: with no entry in ``_pn_interaction_state`` both
|
|
868
|
+
# overrides reproduce stock UIView behavior exactly, and it means the
|
|
869
|
+
# props can appear, change, or disappear on any later render without
|
|
870
|
+
# recreating the view. Leaf handlers (labels, controls) keep their
|
|
871
|
+
# stock classes; if the props show up there, ``_apply_interaction_props``
|
|
872
|
+
# warns once and the props are ignored.
|
|
873
|
+
#
|
|
874
|
+
# The overrides read per-view state synchronously from
|
|
875
|
+
# ``_pn_interaction_state``, a Python dict keyed by the raw view
|
|
876
|
+
# pointer (the ``self`` of every IMP), following the same routing
|
|
877
|
+
# pattern as ``_pn_action_handlers``.
|
|
878
|
+
#
|
|
879
|
+
# Semantics (matching React Native and the Android backend):
|
|
880
|
+
#
|
|
881
|
+
# - ``"none"``: the view and its whole subtree are transparent to
|
|
882
|
+
# touches (``hitTest`` returns nil, ``pointInside`` returns NO).
|
|
883
|
+
# - ``"box_none"``: the view itself never receives touches, but
|
|
884
|
+
# subviews still do (``hitTest`` returns the subview result and
|
|
885
|
+
# converts a self result to nil).
|
|
886
|
+
# - ``"box_only"``: the view receives touches, subviews never do.
|
|
887
|
+
# - ``hit_slop`` expands ``pointInside`` by the slop on each edge.
|
|
888
|
+
# As on every platform, the expanded area only works where the
|
|
889
|
+
# parent's own bounds still contain the point.
|
|
890
|
+
|
|
891
|
+
# Maps view ptr -> {"pointer_events": mode or None,
|
|
892
|
+
# "hit_slop": (top, left, bottom, right) or None}.
|
|
893
|
+
_pn_interaction_state: Dict[int, Dict[str, Any]] = {}
|
|
894
|
+
|
|
895
|
+
_POINTER_EVENT_MODES = ("none", "box_none", "box_only")
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _hit_slop_insets(value: Any) -> Optional[Tuple[float, float, float, float]]:
|
|
899
|
+
"""Normalize a ``hit_slop`` prop into ``(top, left, bottom, right)`` points.
|
|
900
|
+
|
|
901
|
+
Accepts a uniform number or a dict with any of ``top`` / ``left`` /
|
|
902
|
+
``bottom`` / ``right``. Returns ``None`` for absent or unparseable
|
|
903
|
+
values.
|
|
904
|
+
"""
|
|
905
|
+
if value is None:
|
|
906
|
+
return None
|
|
907
|
+
if isinstance(value, dict):
|
|
908
|
+
try:
|
|
909
|
+
return (
|
|
910
|
+
float(value.get("top", 0.0) or 0.0),
|
|
911
|
+
float(value.get("left", 0.0) or 0.0),
|
|
912
|
+
float(value.get("bottom", 0.0) or 0.0),
|
|
913
|
+
float(value.get("right", 0.0) or 0.0),
|
|
914
|
+
)
|
|
915
|
+
except Exception:
|
|
916
|
+
return None
|
|
917
|
+
try:
|
|
918
|
+
uniform = float(value)
|
|
919
|
+
except Exception:
|
|
920
|
+
return None
|
|
921
|
+
return (uniform, uniform, uniform, uniform)
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
_INTERACTIVE_POINT_INSIDE_TYPE = _ct.CFUNCTYPE(_ct.c_bool, _ct.c_void_p, _ct.c_void_p, _PNCGPoint, _ct.c_void_p)
|
|
925
|
+
_INTERACTIVE_HITTEST_TYPE = _ct.CFUNCTYPE(_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _PNCGPoint, _ct.c_void_p)
|
|
926
|
+
|
|
927
|
+
_SEL_HITTEST_WITH_EVENT = _sel_reg(b"hitTest:withEvent:")
|
|
928
|
+
|
|
929
|
+
# UIView's own ``hitTest:withEvent:`` IMP, called directly (the
|
|
930
|
+
# superclass is known to be exactly UIView, so this is equivalent to a
|
|
931
|
+
# ``objc_msgSendSuper`` call without the extra struct plumbing). Bound
|
|
932
|
+
# after the class registration below.
|
|
933
|
+
_uiview_hittest_imp: Any = None
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def _interactive_point_inside_imp(self_ptr: int, _cmd_ptr: int, point: Any, _event_ptr: int) -> bool:
|
|
937
|
+
"""``pointInside:withEvent:`` honoring pointer_events and hit_slop."""
|
|
938
|
+
try:
|
|
939
|
+
info = _pn_interaction_state.get(int(self_ptr or 0)) or {}
|
|
940
|
+
if info.get("pointer_events") == "none":
|
|
941
|
+
return False
|
|
942
|
+
top = left = bottom = right = 0.0
|
|
943
|
+
slop = info.get("hit_slop")
|
|
944
|
+
if slop is not None:
|
|
945
|
+
top, left, bottom, right = slop
|
|
946
|
+
view = ObjCInstance(_ct.c_void_p(self_ptr))
|
|
947
|
+
bounds = view.bounds
|
|
948
|
+
w = float(bounds.size.width)
|
|
949
|
+
h = float(bounds.size.height)
|
|
950
|
+
px = float(point.x)
|
|
951
|
+
py = float(point.y)
|
|
952
|
+
return (-left <= px <= w + right) and (-top <= py <= h + bottom)
|
|
953
|
+
except Exception:
|
|
954
|
+
return False
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _interactive_hittest_imp(self_ptr: int, _cmd_ptr: int, point: Any, event_ptr: int) -> int:
|
|
958
|
+
"""``hitTest:withEvent:`` honoring the pointer_events mode.
|
|
959
|
+
|
|
960
|
+
Delegates to UIView's stock implementation (which consults our
|
|
961
|
+
``pointInside:`` override, so hit_slop is already accounted for),
|
|
962
|
+
then filters the result per the mode.
|
|
963
|
+
"""
|
|
964
|
+
try:
|
|
965
|
+
info = _pn_interaction_state.get(int(self_ptr or 0)) or {}
|
|
966
|
+
mode = info.get("pointer_events")
|
|
967
|
+
if mode == "none":
|
|
968
|
+
return 0
|
|
969
|
+
if _uiview_hittest_imp is None:
|
|
970
|
+
return 0
|
|
971
|
+
result = int(_uiview_hittest_imp(self_ptr, _SEL_HITTEST_WITH_EVENT, point, event_ptr) or 0)
|
|
972
|
+
if mode == "box_none" and result == int(self_ptr or 0):
|
|
973
|
+
return 0
|
|
974
|
+
if mode == "box_only":
|
|
975
|
+
return int(self_ptr or 0) if result else 0
|
|
976
|
+
return result
|
|
977
|
+
except Exception:
|
|
978
|
+
return 0
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
_interactive_point_inside_imp_ref = _INTERACTIVE_POINT_INSIDE_TYPE(_interactive_point_inside_imp)
|
|
982
|
+
_interactive_hittest_imp_ref = _INTERACTIVE_HITTEST_TYPE(_interactive_hittest_imp)
|
|
983
|
+
|
|
984
|
+
_PN_INTERACTIVE_VIEW_CLS = _alloc_cls(_UIVIEW_CLS, b"_PNInteractiveViewCTypes", 0) if _UIVIEW_CLS else None
|
|
985
|
+
if _PN_INTERACTIVE_VIEW_CLS:
|
|
986
|
+
_add_method(
|
|
987
|
+
_PN_INTERACTIVE_VIEW_CLS,
|
|
988
|
+
_sel_reg(b"pointInside:withEvent:"),
|
|
989
|
+
_ct.cast(_interactive_point_inside_imp_ref, _ct.c_void_p),
|
|
990
|
+
b"c@:{CGPoint=dd}@",
|
|
991
|
+
)
|
|
992
|
+
_add_method(
|
|
993
|
+
_PN_INTERACTIVE_VIEW_CLS,
|
|
994
|
+
_SEL_HITTEST_WITH_EVENT,
|
|
995
|
+
_ct.cast(_interactive_hittest_imp_ref, _ct.c_void_p),
|
|
996
|
+
b"@@:{CGPoint=dd}@",
|
|
997
|
+
)
|
|
998
|
+
_reg_cls(_PN_INTERACTIVE_VIEW_CLS)
|
|
999
|
+
try:
|
|
1000
|
+
_raw_hittest = _get_method_imp(_UIVIEW_CLS, _SEL_HITTEST_WITH_EVENT)
|
|
1001
|
+
if _raw_hittest:
|
|
1002
|
+
_uiview_hittest_imp = _ct.cast(_ct.c_void_p(_raw_hittest), _INTERACTIVE_HITTEST_TYPE)
|
|
1003
|
+
except Exception:
|
|
1004
|
+
_uiview_hittest_imp = None
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def _new_container_view() -> Any:
|
|
1008
|
+
"""Instantiate the hit-test-aware container class (UIView fallback).
|
|
1009
|
+
|
|
1010
|
+
When the ``_PNInteractiveViewCTypes`` registration is unavailable
|
|
1011
|
+
the container degrades to a plain ``UIView``: everything works
|
|
1012
|
+
except ``pointer_events`` / ``hit_slop``, which warn when applied.
|
|
1013
|
+
"""
|
|
1014
|
+
view = None
|
|
1015
|
+
if _PN_INTERACTIVE_VIEW_CLS:
|
|
1016
|
+
try:
|
|
1017
|
+
view = ObjCClass("_PNInteractiveViewCTypes").alloc().init()
|
|
1018
|
+
except Exception:
|
|
1019
|
+
view = None
|
|
1020
|
+
if view is None:
|
|
1021
|
+
view = ObjCClass("UIView").alloc().init()
|
|
1022
|
+
view.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
1023
|
+
return view
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _apply_interaction_props(view: Any, props: Dict[str, Any]) -> None:
|
|
1027
|
+
"""Record pointer_events / hit_slop where the ObjC overrides read them."""
|
|
1028
|
+
ptr = _objc_ptr(view)
|
|
1029
|
+
if ptr is None:
|
|
1030
|
+
return
|
|
1031
|
+
entry = _pn_interaction_state.setdefault(ptr, {})
|
|
1032
|
+
if "pointer_events" in props:
|
|
1033
|
+
mode = props["pointer_events"]
|
|
1034
|
+
entry["pointer_events"] = mode if mode in _POINTER_EVENT_MODES else None
|
|
1035
|
+
if "hit_slop" in props:
|
|
1036
|
+
entry["hit_slop"] = _hit_slop_insets(props["hit_slop"])
|
|
1037
|
+
if entry.get("pointer_events") is None and entry.get("hit_slop") is None:
|
|
1038
|
+
# Both props removed (or "auto"): drop the entry so the
|
|
1039
|
+
# overrides take their stock-behavior fast path.
|
|
1040
|
+
_pn_interaction_state.pop(ptr, None)
|
|
1041
|
+
return
|
|
1042
|
+
is_custom = False
|
|
1043
|
+
try:
|
|
1044
|
+
if _PN_INTERACTIVE_VIEW_CLS:
|
|
1045
|
+
is_custom = bool(view.isKindOfClass_(ObjCClass("_PNInteractiveViewCTypes")))
|
|
1046
|
+
except Exception:
|
|
1047
|
+
is_custom = False
|
|
1048
|
+
if not is_custom:
|
|
1049
|
+
diagnostics.warn_once(
|
|
1050
|
+
"pointer_events / hit_slop only work on container elements "
|
|
1051
|
+
"(View, Row, Column, Pressable) on iOS; this element's native "
|
|
1052
|
+
"class doesn't override hit-testing, so the props are ignored",
|
|
1053
|
+
key=f"interaction_props:{_tag_of(view)}",
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
|
|
675
1057
|
# ======================================================================
|
|
676
1058
|
# Retention + shared targets
|
|
677
1059
|
# ======================================================================
|
|
@@ -683,17 +1065,42 @@ _pn_retained_views: list = []
|
|
|
683
1065
|
# Simultaneous-recognition gesture delegate (raw libobjc)
|
|
684
1066
|
# ======================================================================
|
|
685
1067
|
#
|
|
686
|
-
#
|
|
687
|
-
#
|
|
688
|
-
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
1068
|
+
# UIKit defaults to recognizer exclusivity, so every recognizer we
|
|
1069
|
+
# create gets this delegate. The answer to
|
|
1070
|
+
# ``gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:``
|
|
1071
|
+
# mirrors the GestureArbiter's semantics on Android/desktop:
|
|
1072
|
+
#
|
|
1073
|
+
# - Recognizers wired by ``_wire_gestures`` carry an entry in
|
|
1074
|
+
# ``_pn_gesture_relations`` (view pointer, spec index, and the spec's
|
|
1075
|
+
# ``"simultaneous"`` index set). Two recognizers on the *same* view
|
|
1076
|
+
# recognize together only when each is in the other's set; a spec
|
|
1077
|
+
# without the key means "simultaneous with everything" (back-compat
|
|
1078
|
+
# for hand-built spec dicts).
|
|
1079
|
+
# - Any pairing involving an unknown recognizer (Pressable's built-in
|
|
1080
|
+
# press trio, a scroll view's pan, recognizers from another view)
|
|
1081
|
+
# keeps the historical always-YES behavior.
|
|
691
1082
|
|
|
692
1083
|
_GESTURE_SIMUL_TYPE = _ct.CFUNCTYPE(_ct.c_bool, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
|
|
693
1084
|
|
|
1085
|
+
# Maps recognizer ptr -> (view ptr, spec index, simultaneous set or None).
|
|
1086
|
+
_pn_gesture_relations: Dict[int, Tuple[int, int, Optional[frozenset]]] = {}
|
|
1087
|
+
|
|
694
1088
|
|
|
695
1089
|
def _gesture_simul_imp(self_ptr: int, cmd_ptr: int, g1_ptr: int, g2_ptr: int) -> bool:
|
|
696
|
-
|
|
1090
|
+
try:
|
|
1091
|
+
a = _pn_gesture_relations.get(int(g1_ptr or 0))
|
|
1092
|
+
b = _pn_gesture_relations.get(int(g2_ptr or 0))
|
|
1093
|
+
if a is None or b is None:
|
|
1094
|
+
return True
|
|
1095
|
+
if a[0] != b[0]:
|
|
1096
|
+
return True
|
|
1097
|
+
sim_a = a[2]
|
|
1098
|
+
sim_b = b[2]
|
|
1099
|
+
if sim_a is None or sim_b is None:
|
|
1100
|
+
return True
|
|
1101
|
+
return b[1] in sim_a and a[1] in sim_b
|
|
1102
|
+
except Exception:
|
|
1103
|
+
return True
|
|
697
1104
|
|
|
698
1105
|
|
|
699
1106
|
_gesture_simul_imp_ref = _GESTURE_SIMUL_TYPE(_gesture_simul_imp)
|
|
@@ -733,7 +1140,7 @@ def _set_recognizer_delegate(recognizer: Any) -> None:
|
|
|
733
1140
|
rec_ptr = recognizer.ptr if hasattr(recognizer, "ptr") else recognizer
|
|
734
1141
|
_objc_msgSend(rec_ptr, _SEL_SET_DELEGATE, delegate)
|
|
735
1142
|
except Exception:
|
|
736
|
-
|
|
1143
|
+
diagnostics.swallowed("ios._set_recognizer_delegate")
|
|
737
1144
|
|
|
738
1145
|
|
|
739
1146
|
# ======================================================================
|
|
@@ -753,6 +1160,7 @@ def _set_recognizer_delegate(recognizer: Any) -> None:
|
|
|
753
1160
|
_pn_action_handlers: Dict[int, Any] = {}
|
|
754
1161
|
|
|
755
1162
|
_ACTION_IMP_TYPE = _ct.CFUNCTYPE(None, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
|
|
1163
|
+
_CONTROL_ACTION_IMP_TYPE = _ct.CFUNCTYPE(None, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
|
|
756
1164
|
|
|
757
1165
|
|
|
758
1166
|
def _action_imp(_self_ptr: int, _cmd_ptr: int, sender_ptr: int) -> None:
|
|
@@ -763,12 +1171,70 @@ def _action_imp(_self_ptr: int, _cmd_ptr: int, sender_ptr: int) -> None:
|
|
|
763
1171
|
try:
|
|
764
1172
|
handler()
|
|
765
1173
|
except Exception:
|
|
766
|
-
|
|
1174
|
+
diagnostics.swallowed("ios._action_imp")
|
|
767
1175
|
|
|
768
1176
|
|
|
769
1177
|
_action_imp_ref = _ACTION_IMP_TYPE(_action_imp)
|
|
770
1178
|
|
|
1179
|
+
# ----------------------------------------------------------------------
|
|
1180
|
+
# UIControl action dedupe
|
|
1181
|
+
# ----------------------------------------------------------------------
|
|
1182
|
+
#
|
|
1183
|
+
# The simulator's XCTest event-injection stack sometimes delivers a
|
|
1184
|
+
# single synthesized tap as *two* back-to-back "send control actions"
|
|
1185
|
+
# bursts for the same UIEvent (~1ms apart; the unified log shows one
|
|
1186
|
+
# UIEvent delivered to one window, then the doubled sends). Every
|
|
1187
|
+
# non-idempotent ``on_press`` then fires twice: reducer counters jump
|
|
1188
|
+
# by two, alerts present twice, pickers reopen after selecting. The
|
|
1189
|
+
# control action selector therefore takes the ``forEvent:`` argument
|
|
1190
|
+
# and drops a second send whose UIEvent timestamp matches the last one
|
|
1191
|
+
# delivered to the same control. Distinct taps (even rapid ones) carry
|
|
1192
|
+
# distinct event timestamps, and continuous ValueChanged streams
|
|
1193
|
+
# (slider drags) tick the timestamp per touch move, so only the
|
|
1194
|
+
# duplicated sends are filtered. Programmatic ``sendActions...`` calls
|
|
1195
|
+
# pass a nil event and are never deduped.
|
|
1196
|
+
|
|
1197
|
+
# Maps control ptr -> UIEvent.timestamp of the last delivered action.
|
|
1198
|
+
_pn_last_control_event_ts: Dict[int, float] = {}
|
|
1199
|
+
|
|
1200
|
+
_objc_msgSend_double = _ct.CFUNCTYPE(_ct.c_double, _ct.c_void_p, _ct.c_void_p)(("objc_msgSend", _libobjc))
|
|
1201
|
+
|
|
1202
|
+
_SEL_TIMESTAMP = _sel_reg(b"timestamp")
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def _control_action_imp(_self_ptr: int, _cmd_ptr: int, sender_ptr: int, event_ptr: int) -> None:
|
|
1206
|
+
"""Raw C callback for every PythonNative UIControl event action."""
|
|
1207
|
+
key = int(sender_ptr or 0)
|
|
1208
|
+
handler = _pn_action_handlers.get(key)
|
|
1209
|
+
if handler is None:
|
|
1210
|
+
return
|
|
1211
|
+
if event_ptr:
|
|
1212
|
+
try:
|
|
1213
|
+
timestamp = float(_objc_msgSend_double(event_ptr, _SEL_TIMESTAMP))
|
|
1214
|
+
except Exception:
|
|
1215
|
+
timestamp = None
|
|
1216
|
+
if timestamp is not None:
|
|
1217
|
+
if _pn_last_control_event_ts.get(key) == timestamp:
|
|
1218
|
+
diagnostics.warn_once(
|
|
1219
|
+
"ios._control_action_imp dropped a duplicated control action "
|
|
1220
|
+
"(one UIEvent, two sends; a simulator event-injection quirk)",
|
|
1221
|
+
key=f"control_action_dedupe:{key}",
|
|
1222
|
+
)
|
|
1223
|
+
return
|
|
1224
|
+
_pn_last_control_event_ts[key] = timestamp
|
|
1225
|
+
try:
|
|
1226
|
+
handler()
|
|
1227
|
+
except Exception:
|
|
1228
|
+
diagnostics.swallowed("ios._control_action_imp")
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
_control_action_imp_ref = _CONTROL_ACTION_IMP_TYPE(_control_action_imp)
|
|
1232
|
+
|
|
1233
|
+
# Gesture recognizers only support zero- or one-argument action
|
|
1234
|
+
# selectors, so recognizers keep the plain variant while controls use
|
|
1235
|
+
# the two-argument ``forEvent:`` variant for the dedupe above.
|
|
771
1236
|
_SEL_ON_ACTION = _sel_reg(b"onPNAction:")
|
|
1237
|
+
_SEL_ON_CONTROL_ACTION = _sel_reg(b"onPNControlAction:forEvent:")
|
|
772
1238
|
|
|
773
1239
|
_PN_ACTION_TARGET_CLS = _alloc_cls(_NS_OBJECT_CLS, b"_PNActionTargetCTypes", 0)
|
|
774
1240
|
if _PN_ACTION_TARGET_CLS:
|
|
@@ -778,6 +1244,12 @@ if _PN_ACTION_TARGET_CLS:
|
|
|
778
1244
|
_ct.cast(_action_imp_ref, _ct.c_void_p),
|
|
779
1245
|
b"v@:@",
|
|
780
1246
|
)
|
|
1247
|
+
_add_method(
|
|
1248
|
+
_PN_ACTION_TARGET_CLS,
|
|
1249
|
+
_SEL_ON_CONTROL_ACTION,
|
|
1250
|
+
_ct.cast(_control_action_imp_ref, _ct.c_void_p),
|
|
1251
|
+
b"v@:@@",
|
|
1252
|
+
)
|
|
781
1253
|
_reg_cls(_PN_ACTION_TARGET_CLS)
|
|
782
1254
|
|
|
783
1255
|
_pn_action_target_ptr: Any = None
|
|
@@ -830,7 +1302,7 @@ def _register_control_action(control: Any, events_mask: int, handler: Any) -> No
|
|
|
830
1302
|
_objc_msgSend.restype = None
|
|
831
1303
|
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_ulong]
|
|
832
1304
|
ctl_ptr = control.ptr if hasattr(control, "ptr") else control
|
|
833
|
-
_objc_msgSend(ctl_ptr, _SEL_ADD_TARGET_ACTION_EVENTS, target_ptr,
|
|
1305
|
+
_objc_msgSend(ctl_ptr, _SEL_ADD_TARGET_ACTION_EVENTS, target_ptr, _SEL_ON_CONTROL_ACTION, events_mask)
|
|
834
1306
|
_pn_action_handlers[_recognizer_ptr(control)] = handler
|
|
835
1307
|
|
|
836
1308
|
|
|
@@ -846,6 +1318,7 @@ def _make_gesture_handler(
|
|
|
846
1318
|
kind: str,
|
|
847
1319
|
index: int,
|
|
848
1320
|
direction: Optional[str] = None,
|
|
1321
|
+
n_pointers: Optional[int] = None,
|
|
849
1322
|
) -> Any:
|
|
850
1323
|
"""Build the action closure emitting one ``gesture:<i>`` payload."""
|
|
851
1324
|
|
|
@@ -864,11 +1337,11 @@ def _make_gesture_handler(
|
|
|
864
1337
|
payload["x"] = float(location.x)
|
|
865
1338
|
payload["y"] = float(location.y)
|
|
866
1339
|
except Exception:
|
|
867
|
-
|
|
1340
|
+
diagnostics.swallowed("ios._make_gesture_handler.handler")
|
|
868
1341
|
try:
|
|
869
1342
|
payload["pointer_count"] = int(rec.numberOfTouches)
|
|
870
1343
|
except Exception:
|
|
871
|
-
|
|
1344
|
+
diagnostics.swallowed("ios._make_gesture_handler.handler")
|
|
872
1345
|
|
|
873
1346
|
if kind == "pan":
|
|
874
1347
|
try:
|
|
@@ -879,23 +1352,30 @@ def _make_gesture_handler(
|
|
|
879
1352
|
payload["velocity_x"] = float(velocity.x)
|
|
880
1353
|
payload["velocity_y"] = float(velocity.y)
|
|
881
1354
|
except Exception:
|
|
882
|
-
|
|
1355
|
+
diagnostics.swallowed("ios._make_gesture_handler.handler")
|
|
883
1356
|
elif kind == "pinch":
|
|
884
1357
|
try:
|
|
885
1358
|
payload["scale"] = float(rec.scale)
|
|
886
1359
|
except Exception:
|
|
887
|
-
|
|
1360
|
+
diagnostics.swallowed("ios._make_gesture_handler.handler")
|
|
888
1361
|
elif kind == "rotation":
|
|
889
1362
|
try:
|
|
890
1363
|
payload["rotation"] = float(rec.rotation)
|
|
891
1364
|
except Exception:
|
|
892
|
-
|
|
1365
|
+
diagnostics.swallowed("ios._make_gesture_handler.handler")
|
|
893
1366
|
elif kind == "swipe":
|
|
894
1367
|
# Discrete: UIKit only calls us on recognition, and only the
|
|
895
1368
|
# recognizer whose direction matched fires, so the bound
|
|
896
1369
|
# per-recognizer direction is the actual swipe direction.
|
|
897
1370
|
payload["state"] = "ended"
|
|
898
1371
|
payload["direction"] = direction
|
|
1372
|
+
elif kind == "fling":
|
|
1373
|
+
# Same discrete shape as swipe. ``numberOfTouches`` is often
|
|
1374
|
+
# 0 by recognition time, so report the configured pointer
|
|
1375
|
+
# requirement (which UIKit enforced) instead.
|
|
1376
|
+
payload["state"] = "ended"
|
|
1377
|
+
payload["direction"] = direction
|
|
1378
|
+
payload["pointer_count"] = int(n_pointers or 1)
|
|
899
1379
|
elif kind == "tap":
|
|
900
1380
|
payload["state"] = "ended"
|
|
901
1381
|
|
|
@@ -907,9 +1387,17 @@ def _make_gesture_handler(
|
|
|
907
1387
|
def _make_recognizer(kind: str, spec: Dict[str, Any]) -> List[Tuple[Any, Optional[str]]]:
|
|
908
1388
|
"""Build the UIGestureRecognizer(s) for one serialized gesture spec.
|
|
909
1389
|
|
|
910
|
-
Swipe with ``direction="any"``
|
|
911
|
-
(
|
|
912
|
-
|
|
1390
|
+
Swipe and fling with ``direction="any"`` need one recognizer per
|
|
1391
|
+
direction (a ``UISwipeGestureRecognizer`` reports no resolved
|
|
1392
|
+
direction, only its configured mask), so this returns
|
|
1393
|
+
``(recognizer, direction)`` pairs; ``direction`` is ``None`` for
|
|
1394
|
+
the other kinds.
|
|
1395
|
+
|
|
1396
|
+
Fling maps to ``UISwipeGestureRecognizer`` with
|
|
1397
|
+
``numberOfTouchesRequired``. The spec's ``min_velocity`` can't be
|
|
1398
|
+
enforced: UIKit exposes no velocity knob on swipe recognizers, so
|
|
1399
|
+
iOS uses the system's own swipe velocity threshold (a documented
|
|
1400
|
+
approximation; Android and desktop honor the exact value).
|
|
913
1401
|
"""
|
|
914
1402
|
out: List[Tuple[Any, Optional[str]]] = []
|
|
915
1403
|
if kind == "tap":
|
|
@@ -917,7 +1405,7 @@ def _make_recognizer(kind: str, spec: Dict[str, Any]) -> List[Tuple[Any, Optiona
|
|
|
917
1405
|
try:
|
|
918
1406
|
rec.setNumberOfTapsRequired_(max(1, int(spec.get("n_taps", 1))))
|
|
919
1407
|
except Exception:
|
|
920
|
-
|
|
1408
|
+
diagnostics.swallowed("ios._make_recognizer")
|
|
921
1409
|
out.append((rec, None))
|
|
922
1410
|
elif kind == "long_press":
|
|
923
1411
|
rec = ObjCClass("UILongPressGestureRecognizer").alloc().init()
|
|
@@ -925,14 +1413,14 @@ def _make_recognizer(kind: str, spec: Dict[str, Any]) -> List[Tuple[Any, Optiona
|
|
|
925
1413
|
rec.setMinimumPressDuration_(float(spec.get("min_duration_ms", 500.0)) / 1000.0)
|
|
926
1414
|
rec.setAllowableMovement_(float(spec.get("max_distance", 12.0)))
|
|
927
1415
|
except Exception:
|
|
928
|
-
|
|
1416
|
+
diagnostics.swallowed("ios._make_recognizer")
|
|
929
1417
|
out.append((rec, None))
|
|
930
1418
|
elif kind == "pan":
|
|
931
1419
|
rec = ObjCClass("UIPanGestureRecognizer").alloc().init()
|
|
932
1420
|
try:
|
|
933
1421
|
rec.setMinimumNumberOfTouches_(max(1, int(spec.get("min_pointers", 1))))
|
|
934
1422
|
except Exception:
|
|
935
|
-
|
|
1423
|
+
diagnostics.swallowed("ios._make_recognizer")
|
|
936
1424
|
out.append((rec, None))
|
|
937
1425
|
elif kind == "swipe":
|
|
938
1426
|
direction = str(spec.get("direction", "any"))
|
|
@@ -942,7 +1430,19 @@ def _make_recognizer(kind: str, spec: Dict[str, Any]) -> List[Tuple[Any, Optiona
|
|
|
942
1430
|
try:
|
|
943
1431
|
rec.setDirection_(_SWIPE_DIRECTIONS[d])
|
|
944
1432
|
except Exception:
|
|
945
|
-
|
|
1433
|
+
diagnostics.swallowed("ios._make_recognizer")
|
|
1434
|
+
out.append((rec, d))
|
|
1435
|
+
elif kind == "fling":
|
|
1436
|
+
direction = str(spec.get("direction", "any"))
|
|
1437
|
+
directions = [direction] if direction in _SWIPE_DIRECTIONS else list(_SWIPE_DIRECTIONS)
|
|
1438
|
+
n_pointers = max(1, int(spec.get("n_pointers", 1)))
|
|
1439
|
+
for d in directions:
|
|
1440
|
+
rec = ObjCClass("UISwipeGestureRecognizer").alloc().init()
|
|
1441
|
+
try:
|
|
1442
|
+
rec.setDirection_(_SWIPE_DIRECTIONS[d])
|
|
1443
|
+
rec.setNumberOfTouchesRequired_(n_pointers)
|
|
1444
|
+
except Exception:
|
|
1445
|
+
diagnostics.swallowed("ios._make_recognizer")
|
|
946
1446
|
out.append((rec, d))
|
|
947
1447
|
elif kind == "pinch":
|
|
948
1448
|
out.append((ObjCClass("UIPinchGestureRecognizer").alloc().init(), None))
|
|
@@ -957,14 +1457,24 @@ def _wire_gestures(view: Any, specs: Any) -> None:
|
|
|
957
1457
|
Re-wiring on update removes previously attached PythonNative
|
|
958
1458
|
recognizers first (configuration changes are rare; correctness
|
|
959
1459
|
beats incremental patching here).
|
|
1460
|
+
|
|
1461
|
+
Composition metadata is applied in two passes: while creating each
|
|
1462
|
+
recognizer its ``(view, index, simultaneous-set)`` triple is
|
|
1463
|
+
recorded in ``_pn_gesture_relations`` for the shared delegate, and
|
|
1464
|
+
once every recognizer exists, each spec's ``"wait_for"`` indices
|
|
1465
|
+
are wired with ``requireGestureRecognizerToFail:`` (every
|
|
1466
|
+
recognizer of the waiting spec requires every recognizer of each
|
|
1467
|
+
target spec to fail; a spec can own several recognizers, e.g. an
|
|
1468
|
+
any-direction swipe).
|
|
960
1469
|
"""
|
|
961
1470
|
state = _state_of(view)
|
|
962
1471
|
for rec in state.get("gesture_recognizers") or []:
|
|
963
1472
|
try:
|
|
964
1473
|
view.removeGestureRecognizer_(rec)
|
|
965
1474
|
except Exception:
|
|
966
|
-
|
|
1475
|
+
diagnostics.swallowed("ios._wire_gestures")
|
|
967
1476
|
_pn_action_handlers.pop(_recognizer_ptr(rec), None)
|
|
1477
|
+
_pn_gesture_relations.pop(_recognizer_ptr(rec), None)
|
|
968
1478
|
state["gesture_recognizers"] = []
|
|
969
1479
|
if not isinstance(specs, (list, tuple)) or not specs:
|
|
970
1480
|
return
|
|
@@ -972,20 +1482,25 @@ def _wire_gestures(view: Any, specs: Any) -> None:
|
|
|
972
1482
|
try:
|
|
973
1483
|
view.setUserInteractionEnabled_(True)
|
|
974
1484
|
except Exception:
|
|
975
|
-
|
|
1485
|
+
diagnostics.swallowed("ios._wire_gestures")
|
|
976
1486
|
|
|
1487
|
+
view_ptr = _objc_ptr(view) or 0
|
|
977
1488
|
recognizers: List[Any] = []
|
|
1489
|
+
recs_by_index: Dict[int, List[Any]] = {}
|
|
978
1490
|
for i, spec in enumerate(specs):
|
|
979
1491
|
if not isinstance(spec, dict):
|
|
980
1492
|
continue
|
|
981
1493
|
kind = str(spec.get("kind", ""))
|
|
982
1494
|
if not kind:
|
|
983
1495
|
continue
|
|
1496
|
+
sim = spec.get("simultaneous")
|
|
1497
|
+
sim_set = None if sim is None else frozenset(int(s) for s in sim)
|
|
1498
|
+
n_pointers = max(1, int(spec.get("n_pointers", 1) or 1)) if kind == "fling" else None
|
|
984
1499
|
for rec, direction in _make_recognizer(kind, spec):
|
|
985
1500
|
try:
|
|
986
1501
|
rec.setCancelsTouchesInView_(False)
|
|
987
1502
|
except Exception:
|
|
988
|
-
|
|
1503
|
+
diagnostics.swallowed("ios._wire_gestures")
|
|
989
1504
|
_set_recognizer_delegate(rec)
|
|
990
1505
|
try:
|
|
991
1506
|
view.addGestureRecognizer_(rec)
|
|
@@ -993,7 +1508,21 @@ def _wire_gestures(view: Any, specs: Any) -> None:
|
|
|
993
1508
|
recognizers.append(rec)
|
|
994
1509
|
except Exception:
|
|
995
1510
|
continue
|
|
996
|
-
|
|
1511
|
+
_pn_gesture_relations[_recognizer_ptr(rec)] = (view_ptr, i, sim_set)
|
|
1512
|
+
recs_by_index.setdefault(i, []).append(rec)
|
|
1513
|
+
_register_action(rec, _make_gesture_handler(rec, view, kind, i, direction, n_pointers))
|
|
1514
|
+
|
|
1515
|
+
for i, spec in enumerate(specs):
|
|
1516
|
+
if not isinstance(spec, dict):
|
|
1517
|
+
continue
|
|
1518
|
+
for target_index in spec.get("wait_for") or ():
|
|
1519
|
+
targets = recs_by_index.get(int(target_index)) or []
|
|
1520
|
+
for waiter in recs_by_index.get(i) or []:
|
|
1521
|
+
for target in targets:
|
|
1522
|
+
try:
|
|
1523
|
+
waiter.requireGestureRecognizerToFail_(target)
|
|
1524
|
+
except Exception:
|
|
1525
|
+
diagnostics.swallowed("ios._wire_gestures")
|
|
997
1526
|
|
|
998
1527
|
state["gesture_recognizers"] = recognizers
|
|
999
1528
|
|
|
@@ -1010,6 +1539,16 @@ def _is_main_thread() -> bool:
|
|
|
1010
1539
|
|
|
1011
1540
|
|
|
1012
1541
|
def _animated_applier_for(prop: str, value: Any) -> Optional[Callable[[Any], None]]:
|
|
1542
|
+
"""Build the per-frame applier for one animated property.
|
|
1543
|
+
|
|
1544
|
+
Color-valued frames (``background_color`` and ``color``) arrive as
|
|
1545
|
+
``"#RRGGBB"`` / ``"#AARRGGBB"`` strings from ``AnimatedInterpolation``
|
|
1546
|
+
color outputs and go through the same ``_uicolor`` parsing the
|
|
1547
|
+
ordinary update path uses. ``rotate`` receives numeric degrees,
|
|
1548
|
+
which is what ``_apply_transform`` expects for bare numbers. An
|
|
1549
|
+
unknown property returns ``None`` and the frame is silently
|
|
1550
|
+
ignored.
|
|
1551
|
+
"""
|
|
1013
1552
|
if prop == "opacity":
|
|
1014
1553
|
v = float(value)
|
|
1015
1554
|
|
|
@@ -1022,6 +1561,18 @@ def _animated_applier_for(prop: str, value: Any) -> Optional[Callable[[Any], Non
|
|
|
1022
1561
|
def _apply(view: Any) -> None:
|
|
1023
1562
|
view.setBackgroundColor_(_uicolor(value))
|
|
1024
1563
|
|
|
1564
|
+
return _apply
|
|
1565
|
+
if prop == "color":
|
|
1566
|
+
|
|
1567
|
+
def _apply(view: Any) -> None:
|
|
1568
|
+
color = _uicolor(value)
|
|
1569
|
+
try:
|
|
1570
|
+
view.setTextColor_(color)
|
|
1571
|
+
except Exception:
|
|
1572
|
+
# UIButton has no setTextColor:; views with neither
|
|
1573
|
+
# selector raise into the caller's swallow.
|
|
1574
|
+
view.setTitleColor_forState_(color, 0)
|
|
1575
|
+
|
|
1025
1576
|
return _apply
|
|
1026
1577
|
if prop in ("translate_x", "translate_y", "scale", "scale_x", "scale_y", "rotate"):
|
|
1027
1578
|
spec = {prop: value}
|
|
@@ -1082,13 +1633,13 @@ def _start_native_animation(view: Any, anim_id: int, prop_name: str, spec: Dict[
|
|
|
1082
1633
|
|
|
1083
1634
|
native_animation_completed(anim_id, bool(finished))
|
|
1084
1635
|
except Exception:
|
|
1085
|
-
|
|
1636
|
+
diagnostics.swallowed("ios._start_native_animation._completion")
|
|
1086
1637
|
|
|
1087
1638
|
def _animations() -> None:
|
|
1088
1639
|
try:
|
|
1089
1640
|
applier_to(view)
|
|
1090
1641
|
except Exception:
|
|
1091
|
-
|
|
1642
|
+
diagnostics.swallowed("ios._start_native_animation._animations")
|
|
1092
1643
|
|
|
1093
1644
|
try:
|
|
1094
1645
|
# Snap to the starting value so the animation covers the full
|
|
@@ -1163,15 +1714,19 @@ class IOSViewHandler(ViewHandler):
|
|
|
1163
1714
|
state = _state_of(native_view)
|
|
1164
1715
|
for rec in state.get("gesture_recognizers") or []:
|
|
1165
1716
|
_pn_action_handlers.pop(_recognizer_ptr(rec), None)
|
|
1717
|
+
_pn_gesture_relations.pop(_recognizer_ptr(rec), None)
|
|
1166
1718
|
# Controls register their own pointer as the action-handler key
|
|
1167
1719
|
# (see _register_control_action); drop it with the view.
|
|
1168
1720
|
_pn_action_handlers.pop(_recognizer_ptr(native_view), None)
|
|
1721
|
+
_pn_last_control_event_ts.pop(_recognizer_ptr(native_view), None)
|
|
1722
|
+
_pn_interaction_state.pop(_objc_ptr(native_view) or 0, None)
|
|
1169
1723
|
_pn_view_border_radius_map.pop(id(native_view), None)
|
|
1724
|
+
_pn_corner_radii_map.pop(id(native_view), None)
|
|
1170
1725
|
_pn_side_border_map.pop(id(native_view), None)
|
|
1171
1726
|
try:
|
|
1172
1727
|
native_view.removeFromSuperview()
|
|
1173
1728
|
except Exception:
|
|
1174
|
-
|
|
1729
|
+
diagnostics.swallowed("ios.IOSViewHandler.destroy")
|
|
1175
1730
|
_forget(native_view)
|
|
1176
1731
|
|
|
1177
1732
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
@@ -1187,7 +1742,7 @@ class IOSViewHandler(ViewHandler):
|
|
|
1187
1742
|
try:
|
|
1188
1743
|
child.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
1189
1744
|
except Exception:
|
|
1190
|
-
|
|
1745
|
+
diagnostics.swallowed("ios.IOSViewHandler.insert_child")
|
|
1191
1746
|
try:
|
|
1192
1747
|
count = len(list(parent.subviews or []))
|
|
1193
1748
|
except Exception:
|
|
@@ -1198,13 +1753,13 @@ class IOSViewHandler(ViewHandler):
|
|
|
1198
1753
|
try:
|
|
1199
1754
|
parent.addSubview_(child)
|
|
1200
1755
|
except Exception:
|
|
1201
|
-
|
|
1756
|
+
diagnostics.swallowed("ios.IOSViewHandler.insert_child")
|
|
1202
1757
|
|
|
1203
1758
|
def remove_child(self, parent: Any, child: Any) -> None:
|
|
1204
1759
|
try:
|
|
1205
1760
|
child.removeFromSuperview()
|
|
1206
1761
|
except Exception:
|
|
1207
|
-
|
|
1762
|
+
diagnostics.swallowed("ios.IOSViewHandler.remove_child")
|
|
1208
1763
|
|
|
1209
1764
|
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1210
1765
|
if native_view is None:
|
|
@@ -1217,11 +1772,14 @@ class IOSViewHandler(ViewHandler):
|
|
|
1217
1772
|
native_view.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
1218
1773
|
native_view.setFrame_(((frame_x, frame_y), (frame_w, frame_h)))
|
|
1219
1774
|
_clamp_view_corner_radius(native_view, frame_w, frame_h)
|
|
1775
|
+
# The per-corner shape mask is bounds-dependent; rebuild it
|
|
1776
|
+
# whenever the frame changes.
|
|
1777
|
+
_update_corner_mask(native_view, frame_w, frame_h)
|
|
1220
1778
|
_update_side_border_layers(native_view, frame_w, frame_h)
|
|
1221
1779
|
try:
|
|
1222
1780
|
_clamp_layer_corner_radius(native_view.layer, frame_w, frame_h)
|
|
1223
1781
|
except Exception:
|
|
1224
|
-
|
|
1782
|
+
diagnostics.swallowed("ios.IOSViewHandler.set_frame")
|
|
1225
1783
|
try:
|
|
1226
1784
|
parent = native_view.superview
|
|
1227
1785
|
parent_cls = ""
|
|
@@ -1238,9 +1796,9 @@ class IOSViewHandler(ViewHandler):
|
|
|
1238
1796
|
content_h = max(float(bounds.size.height), frame_y + frame_h)
|
|
1239
1797
|
parent.setContentSize_((content_w, content_h))
|
|
1240
1798
|
except Exception:
|
|
1241
|
-
|
|
1799
|
+
diagnostics.swallowed("ios.IOSViewHandler.set_frame")
|
|
1242
1800
|
except Exception:
|
|
1243
|
-
|
|
1801
|
+
diagnostics.swallowed("ios.IOSViewHandler.set_frame")
|
|
1244
1802
|
|
|
1245
1803
|
def measure_intrinsic(
|
|
1246
1804
|
self,
|
|
@@ -1261,7 +1819,14 @@ class IOSViewHandler(ViewHandler):
|
|
|
1261
1819
|
return (0.0, 0.0)
|
|
1262
1820
|
|
|
1263
1821
|
def set_animated_property(self, native_view: Any, prop_name: str, value: Any) -> None:
|
|
1264
|
-
"""Apply one Python-driven animation frame immediately.
|
|
1822
|
+
"""Apply one Python-driven animation frame immediately.
|
|
1823
|
+
|
|
1824
|
+
Accepts floats for ``opacity`` and the transform channels,
|
|
1825
|
+
numeric degrees for ``rotate``, and ``"#RRGGBB"`` /
|
|
1826
|
+
``"#AARRGGBB"`` strings for ``background_color`` and ``color``
|
|
1827
|
+
(see ``_animated_applier_for``). Unknown properties are
|
|
1828
|
+
silently ignored.
|
|
1829
|
+
"""
|
|
1265
1830
|
if native_view is None:
|
|
1266
1831
|
return
|
|
1267
1832
|
try:
|
|
@@ -1273,7 +1838,7 @@ class IOSViewHandler(ViewHandler):
|
|
|
1273
1838
|
try:
|
|
1274
1839
|
applier(native_view)
|
|
1275
1840
|
except Exception:
|
|
1276
|
-
|
|
1841
|
+
diagnostics.swallowed("ios.IOSViewHandler.set_animated_property")
|
|
1277
1842
|
|
|
1278
1843
|
def start_animation(
|
|
1279
1844
|
self,
|
|
@@ -1312,7 +1877,7 @@ class IOSViewHandler(ViewHandler):
|
|
|
1312
1877
|
try:
|
|
1313
1878
|
view.layer.removeAllAnimations()
|
|
1314
1879
|
except Exception:
|
|
1315
|
-
|
|
1880
|
+
diagnostics.swallowed("ios.IOSViewHandler.cancel_animation")
|
|
1316
1881
|
return value
|
|
1317
1882
|
|
|
1318
1883
|
|
|
@@ -1322,17 +1887,18 @@ class IOSViewHandler(ViewHandler):
|
|
|
1322
1887
|
|
|
1323
1888
|
|
|
1324
1889
|
class FlexContainerHandler(IOSViewHandler):
|
|
1325
|
-
"""Container for flex layout, a
|
|
1890
|
+
"""Container for flex layout, a hit-test-aware ``UIView`` subclass.
|
|
1326
1891
|
|
|
1327
1892
|
All flex semantics (direction, alignment, distribution, padding)
|
|
1328
1893
|
are computed by the layout engine and applied via
|
|
1329
1894
|
[`set_frame`][pythonnative.native_views.ios.IOSViewHandler.set_frame].
|
|
1895
|
+
The view is a ``_PNInteractiveViewCTypes`` instance so
|
|
1896
|
+
``pointer_events`` and ``hit_slop`` can be applied on any render
|
|
1897
|
+
without recreating the view (see the interaction-props section).
|
|
1330
1898
|
"""
|
|
1331
1899
|
|
|
1332
1900
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
1333
|
-
|
|
1334
|
-
v.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
1335
|
-
return v
|
|
1901
|
+
return _new_container_view()
|
|
1336
1902
|
|
|
1337
1903
|
|
|
1338
1904
|
# ======================================================================
|
|
@@ -1400,7 +1966,7 @@ class TextHandler(IOSViewHandler):
|
|
|
1400
1966
|
if desc is not None:
|
|
1401
1967
|
font = UIFont.fontWithDescriptor_size_(desc, size)
|
|
1402
1968
|
except Exception:
|
|
1403
|
-
|
|
1969
|
+
diagnostics.swallowed("ios.TextHandler._font_for")
|
|
1404
1970
|
return font
|
|
1405
1971
|
|
|
1406
1972
|
_SPAN_REBUILD_KEYS = (
|
|
@@ -1463,7 +2029,7 @@ class TextHandler(IOSViewHandler):
|
|
|
1463
2029
|
try:
|
|
1464
2030
|
label.setAlpha_(float(props["opacity"]))
|
|
1465
2031
|
except Exception:
|
|
1466
|
-
|
|
2032
|
+
diagnostics.swallowed("ios.TextHandler._apply")
|
|
1467
2033
|
|
|
1468
2034
|
def _apply_spans(self, label: Any, merged: Dict[str, Any]) -> None:
|
|
1469
2035
|
"""Render a rich-text span list as one NSAttributedString.
|
|
@@ -1525,13 +2091,13 @@ class TextHandler(IOSViewHandler):
|
|
|
1525
2091
|
elif span.get("text_decoration") == "line_through":
|
|
1526
2092
|
attr.addAttribute_value_range_("NSStrikethrough", 1, rng)
|
|
1527
2093
|
except Exception:
|
|
1528
|
-
|
|
2094
|
+
diagnostics.swallowed("ios.TextHandler._apply_spans")
|
|
1529
2095
|
label.setAttributedText_(attr)
|
|
1530
2096
|
except Exception:
|
|
1531
2097
|
try:
|
|
1532
2098
|
label.setText_(str(merged.get("text") or ""))
|
|
1533
2099
|
except Exception:
|
|
1534
|
-
|
|
2100
|
+
diagnostics.swallowed("ios.TextHandler._apply_spans")
|
|
1535
2101
|
|
|
1536
2102
|
def _apply_attributed(self, label: Any, props: Dict[str, Any]) -> None:
|
|
1537
2103
|
"""Re-render the label's text as an NSAttributedString.
|
|
@@ -1576,7 +2142,7 @@ class TextHandler(IOSViewHandler):
|
|
|
1576
2142
|
attr.addAttribute_value_range_("NSStrikethrough", 1, full_range)
|
|
1577
2143
|
label.setAttributedText_(attr)
|
|
1578
2144
|
except Exception:
|
|
1579
|
-
|
|
2145
|
+
diagnostics.swallowed("ios.TextHandler._apply_attributed")
|
|
1580
2146
|
|
|
1581
2147
|
|
|
1582
2148
|
class ButtonHandler(IOSViewHandler):
|
|
@@ -1632,7 +2198,7 @@ class ButtonHandler(IOSViewHandler):
|
|
|
1632
2198
|
try:
|
|
1633
2199
|
btn.setAlpha_(float(props["opacity"]))
|
|
1634
2200
|
except Exception:
|
|
1635
|
-
|
|
2201
|
+
diagnostics.swallowed("ios.ButtonHandler._apply")
|
|
1636
2202
|
|
|
1637
2203
|
|
|
1638
2204
|
# ``scrollViewDidScroll:`` hands the delegate a ``UIScrollView*``. rubicon's
|
|
@@ -1718,24 +2284,24 @@ class ScrollViewHandler(IOSViewHandler):
|
|
|
1718
2284
|
sv.setShowsVerticalScrollIndicator_(visible)
|
|
1719
2285
|
sv.setShowsHorizontalScrollIndicator_(visible)
|
|
1720
2286
|
except Exception:
|
|
1721
|
-
|
|
2287
|
+
diagnostics.swallowed("ios.ScrollViewHandler._apply")
|
|
1722
2288
|
if "paging_enabled" in props:
|
|
1723
2289
|
try:
|
|
1724
2290
|
sv.setPagingEnabled_(bool(props["paging_enabled"]))
|
|
1725
2291
|
except Exception:
|
|
1726
|
-
|
|
2292
|
+
diagnostics.swallowed("ios.ScrollViewHandler._apply")
|
|
1727
2293
|
if "bounces" in props:
|
|
1728
2294
|
b = props["bounces"]
|
|
1729
2295
|
try:
|
|
1730
2296
|
sv.setBounces_(True if b is None else bool(b))
|
|
1731
2297
|
except Exception:
|
|
1732
|
-
|
|
2298
|
+
diagnostics.swallowed("ios.ScrollViewHandler._apply")
|
|
1733
2299
|
if "keyboard_dismiss_mode" in props and props["keyboard_dismiss_mode"] is not None:
|
|
1734
2300
|
mapping = {"none": 0, "on_drag": 1, "interactive": 2}
|
|
1735
2301
|
try:
|
|
1736
2302
|
sv.setKeyboardDismissMode_(mapping.get(props["keyboard_dismiss_mode"], 0))
|
|
1737
2303
|
except Exception:
|
|
1738
|
-
|
|
2304
|
+
diagnostics.swallowed("ios.ScrollViewHandler._apply")
|
|
1739
2305
|
|
|
1740
2306
|
def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
1741
2307
|
if name == "scroll_to_offset":
|
|
@@ -1745,7 +2311,7 @@ class ScrollViewHandler(IOSViewHandler):
|
|
|
1745
2311
|
try:
|
|
1746
2312
|
native_view.setContentOffset_animated_((x, y), animated)
|
|
1747
2313
|
except Exception:
|
|
1748
|
-
|
|
2314
|
+
diagnostics.swallowed("ios.ScrollViewHandler.command")
|
|
1749
2315
|
return None
|
|
1750
2316
|
if name == "scroll_to_end":
|
|
1751
2317
|
animated = args.get("animated", True) is not False
|
|
@@ -1760,7 +2326,7 @@ class ScrollViewHandler(IOSViewHandler):
|
|
|
1760
2326
|
offset = (target_x, 0.0) if horizontal else (0.0, target_y)
|
|
1761
2327
|
native_view.setContentOffset_animated_(offset, animated)
|
|
1762
2328
|
except Exception:
|
|
1763
|
-
|
|
2329
|
+
diagnostics.swallowed("ios.ScrollViewHandler.command")
|
|
1764
2330
|
return None
|
|
1765
2331
|
if name == "get_scroll_offset":
|
|
1766
2332
|
try:
|
|
@@ -1796,10 +2362,11 @@ class ScrollViewHandler(IOSViewHandler):
|
|
|
1796
2362
|
if existing is not None:
|
|
1797
2363
|
existing.endRefreshing()
|
|
1798
2364
|
_pn_action_handlers.pop(_recognizer_ptr(existing), None)
|
|
2365
|
+
_pn_last_control_event_ts.pop(_recognizer_ptr(existing), None)
|
|
1799
2366
|
sv.setRefreshControl_(None)
|
|
1800
2367
|
sv.setAlwaysBounceVertical_(False)
|
|
1801
2368
|
except Exception:
|
|
1802
|
-
|
|
2369
|
+
diagnostics.swallowed("ios.ScrollViewHandler._apply_refresh")
|
|
1803
2370
|
return
|
|
1804
2371
|
try:
|
|
1805
2372
|
existing = sv.refreshControl
|
|
@@ -1816,7 +2383,7 @@ class ScrollViewHandler(IOSViewHandler):
|
|
|
1816
2383
|
try:
|
|
1817
2384
|
sv.setAlwaysBounceVertical_(True)
|
|
1818
2385
|
except Exception:
|
|
1819
|
-
|
|
2386
|
+
diagnostics.swallowed("ios.ScrollViewHandler._apply_refresh")
|
|
1820
2387
|
existing = rc
|
|
1821
2388
|
refreshing = bool(spec.get("refreshing")) if isinstance(spec, dict) else False
|
|
1822
2389
|
if refreshing:
|
|
@@ -1824,7 +2391,7 @@ class ScrollViewHandler(IOSViewHandler):
|
|
|
1824
2391
|
else:
|
|
1825
2392
|
existing.endRefreshing()
|
|
1826
2393
|
except Exception:
|
|
1827
|
-
|
|
2394
|
+
diagnostics.swallowed("ios.ScrollViewHandler._apply_refresh")
|
|
1828
2395
|
|
|
1829
2396
|
|
|
1830
2397
|
class ImageHandler(IOSViewHandler):
|
|
@@ -1842,12 +2409,12 @@ class ImageHandler(IOSViewHandler):
|
|
|
1842
2409
|
try:
|
|
1843
2410
|
iv.setTintColor_(_uicolor(props["tint_color"]))
|
|
1844
2411
|
except Exception:
|
|
1845
|
-
|
|
2412
|
+
diagnostics.swallowed("ios.ImageHandler._apply")
|
|
1846
2413
|
if "placeholder_color" in props and props["placeholder_color"] is not None:
|
|
1847
2414
|
try:
|
|
1848
2415
|
iv.setBackgroundColor_(_uicolor(props["placeholder_color"]))
|
|
1849
2416
|
except Exception:
|
|
1850
|
-
|
|
2417
|
+
diagnostics.swallowed("ios.ImageHandler._apply")
|
|
1851
2418
|
if "source" in props and props["source"]:
|
|
1852
2419
|
self._load_source(iv, str(props["source"]))
|
|
1853
2420
|
if "scale_type" in props and props["scale_type"]:
|
|
@@ -1873,7 +2440,7 @@ class ImageHandler(IOSViewHandler):
|
|
|
1873
2440
|
w, h = max_width, h * scale
|
|
1874
2441
|
return (w, h)
|
|
1875
2442
|
except Exception:
|
|
1876
|
-
|
|
2443
|
+
diagnostics.swallowed("ios.ImageHandler.measure_intrinsic")
|
|
1877
2444
|
return (0.0, 0.0)
|
|
1878
2445
|
|
|
1879
2446
|
def _load_source(self, iv: Any, source: str) -> None:
|
|
@@ -1894,7 +2461,7 @@ class ImageHandler(IOSViewHandler):
|
|
|
1894
2461
|
else:
|
|
1895
2462
|
_fire(iv, "on_error", f"image {source!r} not found")
|
|
1896
2463
|
except Exception:
|
|
1897
|
-
|
|
2464
|
+
diagnostics.swallowed("ios.ImageHandler._load_source")
|
|
1898
2465
|
|
|
1899
2466
|
def _load_data_uri(self, iv: Any, source: str) -> None:
|
|
1900
2467
|
"""Decode an inline ``data:`` URI (base64 payload) into the view."""
|
|
@@ -1942,7 +2509,7 @@ class ImageHandler(IOSViewHandler):
|
|
|
1942
2509
|
iv.setImage_(image)
|
|
1943
2510
|
_fire(iv, "on_load")
|
|
1944
2511
|
except Exception:
|
|
1945
|
-
|
|
2512
|
+
diagnostics.swallowed("ios.ImageHandler._load_async.on_ready")
|
|
1946
2513
|
|
|
1947
2514
|
def on_error(message: str) -> None:
|
|
1948
2515
|
if _state_of(iv).get("pending_uri") == source:
|
|
@@ -1950,7 +2517,7 @@ class ImageHandler(IOSViewHandler):
|
|
|
1950
2517
|
|
|
1951
2518
|
fetch(source, on_ready, on_error)
|
|
1952
2519
|
except Exception:
|
|
1953
|
-
|
|
2520
|
+
diagnostics.swallowed("ios.ImageHandler._load_async")
|
|
1954
2521
|
|
|
1955
2522
|
def _decode_downsampled(self, iv: Any, path: str) -> Any:
|
|
1956
2523
|
"""Decode a file, scaling down to the view's bounds if oversized.
|
|
@@ -2052,7 +2619,7 @@ def _tf_on_change_imp(self_ptr: int, _cmd: int, sender_ptr: int) -> None:
|
|
|
2052
2619
|
try:
|
|
2053
2620
|
view.setText_(text)
|
|
2054
2621
|
except Exception:
|
|
2055
|
-
|
|
2622
|
+
diagnostics.swallowed("ios._tf_on_change_imp")
|
|
2056
2623
|
finally:
|
|
2057
2624
|
state["suppress"] = False
|
|
2058
2625
|
_fire(view, "on_change", text)
|
|
@@ -2077,7 +2644,7 @@ def _tf_should_return_imp(self_ptr: int, _cmd: int, tf_ptr: int) -> bool:
|
|
|
2077
2644
|
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p]
|
|
2078
2645
|
_objc_msgSend(_ct.c_void_p(int(tf_ptr or 0)), _SEL_RESIGN_FIRST_RESPONDER)
|
|
2079
2646
|
except Exception:
|
|
2080
|
-
|
|
2647
|
+
diagnostics.swallowed("ios._tf_should_return_imp")
|
|
2081
2648
|
return True
|
|
2082
2649
|
|
|
2083
2650
|
|
|
@@ -2244,7 +2811,7 @@ class TextInputHandler(IOSViewHandler):
|
|
|
2244
2811
|
)
|
|
2245
2812
|
view.setAttributedPlaceholder_(attr)
|
|
2246
2813
|
except Exception:
|
|
2247
|
-
|
|
2814
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2248
2815
|
if "font_size" in props and props["font_size"] is not None:
|
|
2249
2816
|
view.setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
|
|
2250
2817
|
if "color" in props and props["color"] is not None:
|
|
@@ -2255,7 +2822,7 @@ class TextInputHandler(IOSViewHandler):
|
|
|
2255
2822
|
try:
|
|
2256
2823
|
view.setSecureTextEntry_(bool(props["secure"]))
|
|
2257
2824
|
except Exception:
|
|
2258
|
-
|
|
2825
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2259
2826
|
if "keyboard_type" in props and props["keyboard_type"] is not None:
|
|
2260
2827
|
mapping = {
|
|
2261
2828
|
"default": 0,
|
|
@@ -2270,19 +2837,19 @@ class TextInputHandler(IOSViewHandler):
|
|
|
2270
2837
|
try:
|
|
2271
2838
|
view.setKeyboardType_(mapping.get(props["keyboard_type"], 0))
|
|
2272
2839
|
except Exception:
|
|
2273
|
-
|
|
2840
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2274
2841
|
if "auto_capitalize" in props and props["auto_capitalize"] is not None:
|
|
2275
2842
|
# UITextAutocapitalizationType: none=0, words=1, sentences=2, all=3.
|
|
2276
2843
|
mapping = {"none": 0, "words": 1, "sentences": 2, "characters": 3}
|
|
2277
2844
|
try:
|
|
2278
2845
|
view.setAutocapitalizationType_(mapping.get(props["auto_capitalize"], 2))
|
|
2279
2846
|
except Exception:
|
|
2280
|
-
|
|
2847
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2281
2848
|
if "auto_correct" in props:
|
|
2282
2849
|
try:
|
|
2283
2850
|
view.setAutocorrectionType_(1 if props["auto_correct"] else 0)
|
|
2284
2851
|
except Exception:
|
|
2285
|
-
|
|
2852
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2286
2853
|
if "return_key_type" in props and props["return_key_type"] is not None:
|
|
2287
2854
|
mapping = {
|
|
2288
2855
|
"default": 0,
|
|
@@ -2299,23 +2866,23 @@ class TextInputHandler(IOSViewHandler):
|
|
|
2299
2866
|
try:
|
|
2300
2867
|
view.setReturnKeyType_(mapping.get(props["return_key_type"], 0))
|
|
2301
2868
|
except Exception:
|
|
2302
|
-
|
|
2869
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2303
2870
|
if "selection_color" in props and props["selection_color"] is not None:
|
|
2304
2871
|
try:
|
|
2305
2872
|
view.setTintColor_(_uicolor(props["selection_color"]))
|
|
2306
2873
|
except Exception:
|
|
2307
|
-
|
|
2874
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2308
2875
|
if "text_content_type" in props:
|
|
2309
2876
|
tct = props["text_content_type"]
|
|
2310
2877
|
try:
|
|
2311
2878
|
view.setTextContentType_(_ui_text_content_type(str(tct)) if tct is not None else None)
|
|
2312
2879
|
except Exception:
|
|
2313
|
-
|
|
2880
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2314
2881
|
if "clear_button" in props and is_field:
|
|
2315
2882
|
try:
|
|
2316
2883
|
view.setClearButtonMode_(1 if props["clear_button"] else 0) # 1 = WhileEditing
|
|
2317
2884
|
except Exception:
|
|
2318
|
-
|
|
2885
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2319
2886
|
# ``editable`` is present only when False (read-only). A removed
|
|
2320
2887
|
# prop arrives as None on update, which means "editable again".
|
|
2321
2888
|
if "editable" in props:
|
|
@@ -2327,12 +2894,12 @@ class TextInputHandler(IOSViewHandler):
|
|
|
2327
2894
|
else:
|
|
2328
2895
|
view.setEditable_(resolved)
|
|
2329
2896
|
except Exception:
|
|
2330
|
-
|
|
2897
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2331
2898
|
if "auto_focus" in props and props["auto_focus"]:
|
|
2332
2899
|
try:
|
|
2333
2900
|
view.becomeFirstResponder()
|
|
2334
2901
|
except Exception:
|
|
2335
|
-
|
|
2902
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2336
2903
|
_apply_view_border(view, props)
|
|
2337
2904
|
_apply_shadow(view, props)
|
|
2338
2905
|
_apply_transform(view, props)
|
|
@@ -2341,7 +2908,7 @@ class TextInputHandler(IOSViewHandler):
|
|
|
2341
2908
|
try:
|
|
2342
2909
|
view.setAlpha_(float(props["opacity"]))
|
|
2343
2910
|
except Exception:
|
|
2344
|
-
|
|
2911
|
+
diagnostics.swallowed("ios.TextInputHandler._apply")
|
|
2345
2912
|
|
|
2346
2913
|
def measure_intrinsic(
|
|
2347
2914
|
self,
|
|
@@ -2357,13 +2924,13 @@ class TextInputHandler(IOSViewHandler):
|
|
|
2357
2924
|
try:
|
|
2358
2925
|
native_view.becomeFirstResponder()
|
|
2359
2926
|
except Exception:
|
|
2360
|
-
|
|
2927
|
+
diagnostics.swallowed("ios.TextInputHandler.command")
|
|
2361
2928
|
return None
|
|
2362
2929
|
if name == "blur":
|
|
2363
2930
|
try:
|
|
2364
2931
|
native_view.resignFirstResponder()
|
|
2365
2932
|
except Exception:
|
|
2366
|
-
|
|
2933
|
+
diagnostics.swallowed("ios.TextInputHandler.command")
|
|
2367
2934
|
return None
|
|
2368
2935
|
if name == "clear":
|
|
2369
2936
|
state = _state_of(native_view)
|
|
@@ -2481,7 +3048,7 @@ class ActivityIndicatorHandler(IOSViewHandler):
|
|
|
2481
3048
|
try:
|
|
2482
3049
|
ai.setActivityIndicatorViewStyle_(style)
|
|
2483
3050
|
except Exception:
|
|
2484
|
-
|
|
3051
|
+
diagnostics.swallowed("ios.ActivityIndicatorHandler._apply")
|
|
2485
3052
|
if "color" in props and props["color"] is not None:
|
|
2486
3053
|
ai.setColor_(_uicolor(props["color"]))
|
|
2487
3054
|
animating = props.get("animating")
|
|
@@ -2518,8 +3085,7 @@ class PressableHandler(IOSViewHandler):
|
|
|
2518
3085
|
"""
|
|
2519
3086
|
|
|
2520
3087
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
2521
|
-
v =
|
|
2522
|
-
v.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
3088
|
+
v = _new_container_view()
|
|
2523
3089
|
v.setUserInteractionEnabled_(True)
|
|
2524
3090
|
self._wire_press(v)
|
|
2525
3091
|
return v
|
|
@@ -2563,7 +3129,7 @@ class PressableHandler(IOSViewHandler):
|
|
|
2563
3129
|
try:
|
|
2564
3130
|
rec.setCancelsTouchesInView_(False)
|
|
2565
3131
|
except Exception:
|
|
2566
|
-
|
|
3132
|
+
diagnostics.swallowed("ios.PressableHandler._wire_press")
|
|
2567
3133
|
_set_recognizer_delegate(rec)
|
|
2568
3134
|
view.addGestureRecognizer_(rec)
|
|
2569
3135
|
rec.retain()
|
|
@@ -2643,7 +3209,7 @@ def _press_feedback(view: Any, pressed: bool) -> None:
|
|
|
2643
3209
|
UIView = ObjCClass("UIView")
|
|
2644
3210
|
UIView.animateWithDuration_animations_(duration, lambda: view.setAlpha_(opacity))
|
|
2645
3211
|
except Exception:
|
|
2646
|
-
|
|
3212
|
+
diagnostics.swallowed("ios._press_feedback")
|
|
2647
3213
|
|
|
2648
3214
|
|
|
2649
3215
|
# ======================================================================
|
|
@@ -2668,7 +3234,7 @@ class ProgressBarHandler(IOSViewHandler):
|
|
|
2668
3234
|
try:
|
|
2669
3235
|
ai.startAnimating()
|
|
2670
3236
|
except Exception:
|
|
2671
|
-
|
|
3237
|
+
diagnostics.swallowed("ios.ProgressBarHandler._build")
|
|
2672
3238
|
return ai
|
|
2673
3239
|
pv = ObjCClass("UIProgressView").alloc().init()
|
|
2674
3240
|
pv.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
@@ -2684,27 +3250,27 @@ class ProgressBarHandler(IOSViewHandler):
|
|
|
2684
3250
|
try:
|
|
2685
3251
|
view.setColor_(_uicolor(props["color"]))
|
|
2686
3252
|
except Exception:
|
|
2687
|
-
|
|
3253
|
+
diagnostics.swallowed("ios.ProgressBarHandler._apply")
|
|
2688
3254
|
try:
|
|
2689
3255
|
view.startAnimating()
|
|
2690
3256
|
except Exception:
|
|
2691
|
-
|
|
3257
|
+
diagnostics.swallowed("ios.ProgressBarHandler._apply")
|
|
2692
3258
|
else:
|
|
2693
3259
|
if "value" in props and props["value"] is not None:
|
|
2694
3260
|
try:
|
|
2695
3261
|
view.setProgress_(float(props["value"]))
|
|
2696
3262
|
except Exception:
|
|
2697
|
-
|
|
3263
|
+
diagnostics.swallowed("ios.ProgressBarHandler._apply")
|
|
2698
3264
|
if "color" in props and props["color"] is not None:
|
|
2699
3265
|
try:
|
|
2700
3266
|
view.setProgressTintColor_(_uicolor(props["color"]))
|
|
2701
3267
|
except Exception:
|
|
2702
|
-
|
|
3268
|
+
diagnostics.swallowed("ios.ProgressBarHandler._apply")
|
|
2703
3269
|
if "track_color" in props and props["track_color"] is not None:
|
|
2704
3270
|
try:
|
|
2705
3271
|
view.setTrackTintColor_(_uicolor(props["track_color"]))
|
|
2706
3272
|
except Exception:
|
|
2707
|
-
|
|
3273
|
+
diagnostics.swallowed("ios.ProgressBarHandler._apply")
|
|
2708
3274
|
_apply_accessibility(view, props)
|
|
2709
3275
|
|
|
2710
3276
|
def measure_intrinsic(
|
|
@@ -2771,7 +3337,7 @@ def _webview_did_finish_imp(self_ptr: int, _cmd_ptr: int, _webview_ptr: int, _na
|
|
|
2771
3337
|
try:
|
|
2772
3338
|
wv.evaluateJavaScript_completionHandler_(str(js), None)
|
|
2773
3339
|
except Exception:
|
|
2774
|
-
|
|
3340
|
+
diagnostics.swallowed("ios._webview_did_finish_imp")
|
|
2775
3341
|
_fire(wv, "on_load", _webview_url(wv))
|
|
2776
3342
|
|
|
2777
3343
|
|
|
@@ -2855,7 +3421,7 @@ class WebViewHandler(IOSViewHandler):
|
|
|
2855
3421
|
try:
|
|
2856
3422
|
cls.declare_property(prop)
|
|
2857
3423
|
except Exception:
|
|
2858
|
-
|
|
3424
|
+
diagnostics.swallowed("ios.WebViewHandler._build")
|
|
2859
3425
|
_pn_wkwebview_declared = True
|
|
2860
3426
|
config = WKWebViewConfiguration.alloc().init()
|
|
2861
3427
|
delegate_ptr = _new_webview_delegate_ptr()
|
|
@@ -2867,14 +3433,14 @@ class WebViewHandler(IOSViewHandler):
|
|
|
2867
3433
|
try:
|
|
2868
3434
|
config.userContentController.addScriptMessageHandler_name_(delegate, "pythonnative")
|
|
2869
3435
|
except Exception:
|
|
2870
|
-
|
|
3436
|
+
diagnostics.swallowed("ios.WebViewHandler._build")
|
|
2871
3437
|
wv = WKWebView.alloc().initWithFrame_configuration_(((0, 0), (0, 0)), config)
|
|
2872
3438
|
wv.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
2873
3439
|
if delegate is not None:
|
|
2874
3440
|
try:
|
|
2875
3441
|
wv.setNavigationDelegate_(delegate)
|
|
2876
3442
|
except Exception:
|
|
2877
|
-
|
|
3443
|
+
diagnostics.swallowed("ios.WebViewHandler._build")
|
|
2878
3444
|
if delegate_ptr:
|
|
2879
3445
|
_pn_webview_state[delegate_ptr] = {"view": wv, "inject_js": None}
|
|
2880
3446
|
self._delegate_ids[_objc_ptr(wv) or 0] = delegate_ptr
|
|
@@ -2907,7 +3473,7 @@ class WebViewHandler(IOSViewHandler):
|
|
|
2907
3473
|
try:
|
|
2908
3474
|
wv.loadHTMLString_baseURL_(str(props["html"]), None)
|
|
2909
3475
|
except Exception:
|
|
2910
|
-
|
|
3476
|
+
diagnostics.swallowed("ios.WebViewHandler._apply")
|
|
2911
3477
|
elif "url" in props and props["url"]:
|
|
2912
3478
|
try:
|
|
2913
3479
|
NSURL = ObjCClass("NSURL")
|
|
@@ -2915,38 +3481,38 @@ class WebViewHandler(IOSViewHandler):
|
|
|
2915
3481
|
url_obj = NSURL.URLWithString_(str(props["url"]))
|
|
2916
3482
|
wv.loadRequest_(NSURLRequest.requestWithURL_(url_obj))
|
|
2917
3483
|
except Exception:
|
|
2918
|
-
|
|
3484
|
+
diagnostics.swallowed("ios.WebViewHandler._apply")
|
|
2919
3485
|
if "scroll_enabled" in props:
|
|
2920
3486
|
enabled = props["scroll_enabled"]
|
|
2921
3487
|
try:
|
|
2922
3488
|
wv.scrollView.setScrollEnabled_(True if enabled is None else bool(enabled))
|
|
2923
3489
|
except Exception:
|
|
2924
|
-
|
|
3490
|
+
diagnostics.swallowed("ios.WebViewHandler._apply")
|
|
2925
3491
|
|
|
2926
3492
|
def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
2927
3493
|
if name == "eval_js":
|
|
2928
3494
|
try:
|
|
2929
3495
|
native_view.evaluateJavaScript_completionHandler_(str(args.get("source", "")), None)
|
|
2930
3496
|
except Exception:
|
|
2931
|
-
|
|
3497
|
+
diagnostics.swallowed("ios.WebViewHandler.command")
|
|
2932
3498
|
return None
|
|
2933
3499
|
if name == "reload":
|
|
2934
3500
|
try:
|
|
2935
3501
|
native_view.reload()
|
|
2936
3502
|
except Exception:
|
|
2937
|
-
|
|
3503
|
+
diagnostics.swallowed("ios.WebViewHandler.command")
|
|
2938
3504
|
return None
|
|
2939
3505
|
if name == "go_back":
|
|
2940
3506
|
try:
|
|
2941
3507
|
native_view.goBack()
|
|
2942
3508
|
except Exception:
|
|
2943
|
-
|
|
3509
|
+
diagnostics.swallowed("ios.WebViewHandler.command")
|
|
2944
3510
|
return None
|
|
2945
3511
|
if name == "go_forward":
|
|
2946
3512
|
try:
|
|
2947
3513
|
native_view.goForward()
|
|
2948
3514
|
except Exception:
|
|
2949
|
-
|
|
3515
|
+
diagnostics.swallowed("ios.WebViewHandler.command")
|
|
2950
3516
|
return None
|
|
2951
3517
|
return None
|
|
2952
3518
|
|
|
@@ -2976,9 +3542,7 @@ class SafeAreaViewHandler(IOSViewHandler):
|
|
|
2976
3542
|
"""Plain container; safe-area insets are applied by the layout engine."""
|
|
2977
3543
|
|
|
2978
3544
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
2979
|
-
|
|
2980
|
-
v.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
2981
|
-
return v
|
|
3545
|
+
return _new_container_view()
|
|
2982
3546
|
|
|
2983
3547
|
|
|
2984
3548
|
# ======================================================================
|
|
@@ -3026,13 +3590,13 @@ class ModalHandler(IOSViewHandler):
|
|
|
3026
3590
|
try:
|
|
3027
3591
|
child.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
3028
3592
|
except Exception:
|
|
3029
|
-
|
|
3593
|
+
diagnostics.swallowed("ios.ModalHandler.insert_child")
|
|
3030
3594
|
try:
|
|
3031
3595
|
content = modal["content_view"]
|
|
3032
3596
|
count = len(list(content.subviews or []))
|
|
3033
3597
|
content.insertSubview_atIndex_(child, max(0, min(index, count)))
|
|
3034
3598
|
except Exception:
|
|
3035
|
-
|
|
3599
|
+
diagnostics.swallowed("ios.ModalHandler.insert_child")
|
|
3036
3600
|
else:
|
|
3037
3601
|
buf = state.setdefault("pending_children", [])
|
|
3038
3602
|
buf.insert(max(0, min(index, len(buf))), child)
|
|
@@ -3041,7 +3605,7 @@ class ModalHandler(IOSViewHandler):
|
|
|
3041
3605
|
try:
|
|
3042
3606
|
child.removeFromSuperview()
|
|
3043
3607
|
except Exception:
|
|
3044
|
-
|
|
3608
|
+
diagnostics.swallowed("ios.ModalHandler.remove_child")
|
|
3045
3609
|
buf = _state_of(parent).get("pending_children")
|
|
3046
3610
|
if buf and child in buf:
|
|
3047
3611
|
buf.remove(child)
|
|
@@ -3082,7 +3646,7 @@ class ModalHandler(IOSViewHandler):
|
|
|
3082
3646
|
content.setFrame_(((0, 0), (bounds.size.width, bounds.size.height)))
|
|
3083
3647
|
content.setAutoresizingMask_(2 | 16) # FlexibleWidth | FlexibleHeight
|
|
3084
3648
|
except Exception:
|
|
3085
|
-
|
|
3649
|
+
diagnostics.swallowed("ios.ModalHandler._present")
|
|
3086
3650
|
|
|
3087
3651
|
# UIModalPresentationStyle: fullScreen=0, pageSheet=1,
|
|
3088
3652
|
# formSheet=2, overCurrentContext=6 (the dimmed overlay).
|
|
@@ -3091,7 +3655,7 @@ class ModalHandler(IOSViewHandler):
|
|
|
3091
3655
|
try:
|
|
3092
3656
|
controller.setModalPresentationStyle_(style_int)
|
|
3093
3657
|
except Exception:
|
|
3094
|
-
|
|
3658
|
+
diagnostics.swallowed("ios.ModalHandler._present")
|
|
3095
3659
|
# For sheet styles, ``dismiss_on_backdrop=False`` locks
|
|
3096
3660
|
# interactive (swipe / outside-tap) dismissal so the modal
|
|
3097
3661
|
# stays put until ``visible`` is driven back to False.
|
|
@@ -3099,7 +3663,7 @@ class ModalHandler(IOSViewHandler):
|
|
|
3099
3663
|
try:
|
|
3100
3664
|
controller.setModalInPresentation_(True)
|
|
3101
3665
|
except Exception:
|
|
3102
|
-
|
|
3666
|
+
diagnostics.swallowed("ios.ModalHandler._present")
|
|
3103
3667
|
|
|
3104
3668
|
def _on_present_complete() -> None:
|
|
3105
3669
|
_fire(placeholder, "on_show")
|
|
@@ -3115,7 +3679,7 @@ class ModalHandler(IOSViewHandler):
|
|
|
3115
3679
|
child.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
3116
3680
|
content.addSubview_(child)
|
|
3117
3681
|
except Exception:
|
|
3118
|
-
|
|
3682
|
+
diagnostics.swallowed("ios.ModalHandler._present")
|
|
3119
3683
|
|
|
3120
3684
|
top = _top_view_controller_for_alert(UIApplication.sharedApplication)
|
|
3121
3685
|
if top is not None:
|
|
@@ -3133,7 +3697,7 @@ class ModalHandler(IOSViewHandler):
|
|
|
3133
3697
|
try:
|
|
3134
3698
|
controller.dismissViewControllerAnimated_completion_(True, None)
|
|
3135
3699
|
except Exception:
|
|
3136
|
-
|
|
3700
|
+
diagnostics.swallowed("ios.ModalHandler._dismiss")
|
|
3137
3701
|
_fire(placeholder, "on_dismiss")
|
|
3138
3702
|
|
|
3139
3703
|
|
|
@@ -3152,10 +3716,6 @@ class ModalHandler(IOSViewHandler):
|
|
|
3152
3716
|
# other delegate classes in this module.
|
|
3153
3717
|
|
|
3154
3718
|
|
|
3155
|
-
class _PNCGPoint(_ct.Structure):
|
|
3156
|
-
_fields_ = [("x", _ct.c_double), ("y", _ct.c_double)]
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
3719
|
_PORTAL_POINT_INSIDE_TYPE = _ct.CFUNCTYPE(_ct.c_bool, _ct.c_void_p, _ct.c_void_p, _PNCGPoint, _ct.c_void_p)
|
|
3160
3720
|
|
|
3161
3721
|
|
|
@@ -3193,7 +3753,6 @@ def _portal_point_inside_imp(self_ptr: int, _cmd_ptr: int, point: Any, _event_pt
|
|
|
3193
3753
|
|
|
3194
3754
|
_portal_point_inside_imp_ref = _PORTAL_POINT_INSIDE_TYPE(_portal_point_inside_imp)
|
|
3195
3755
|
|
|
3196
|
-
_UIVIEW_CLS = _get_cls(b"UIView")
|
|
3197
3756
|
_PN_PORTAL_VIEW_CLS = _alloc_cls(_UIVIEW_CLS, b"_PNPortalViewCTypes", 0) if _UIVIEW_CLS else None
|
|
3198
3757
|
if _PN_PORTAL_VIEW_CLS:
|
|
3199
3758
|
_add_method(
|
|
@@ -3270,7 +3829,7 @@ class PortalHandler(IOSViewHandler):
|
|
|
3270
3829
|
host.addSubview_(overlay)
|
|
3271
3830
|
self._sync_overlay_frame(overlay, host)
|
|
3272
3831
|
except Exception:
|
|
3273
|
-
|
|
3832
|
+
diagnostics.swallowed("ios.PortalHandler._ensure_attached")
|
|
3274
3833
|
|
|
3275
3834
|
@staticmethod
|
|
3276
3835
|
def _sync_overlay_frame(overlay: Any, host: Any) -> None:
|
|
@@ -3286,7 +3845,7 @@ class PortalHandler(IOSViewHandler):
|
|
|
3286
3845
|
overlay.setFrame_(((left, top), (w, h)))
|
|
3287
3846
|
overlay.setAutoresizingMask_(2 | 16) # FlexibleWidth | FlexibleHeight
|
|
3288
3847
|
except Exception:
|
|
3289
|
-
|
|
3848
|
+
diagnostics.swallowed("ios.PortalHandler._sync_overlay_frame")
|
|
3290
3849
|
|
|
3291
3850
|
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
3292
3851
|
self._ensure_attached(parent)
|
|
@@ -3313,7 +3872,7 @@ class PortalHandler(IOSViewHandler):
|
|
|
3313
3872
|
try:
|
|
3314
3873
|
overlay.release()
|
|
3315
3874
|
except Exception:
|
|
3316
|
-
|
|
3875
|
+
diagnostics.swallowed("ios.PortalHandler._teardown")
|
|
3317
3876
|
|
|
3318
3877
|
|
|
3319
3878
|
# ======================================================================
|
|
@@ -3347,7 +3906,7 @@ class StatusBarHandler(IOSViewHandler):
|
|
|
3347
3906
|
mapping = {"default": 3, "light": 1, "dark": 3}
|
|
3348
3907
|
app.setStatusBarStyle_animated_(mapping.get(props["bar_style"], 0), True)
|
|
3349
3908
|
except Exception:
|
|
3350
|
-
|
|
3909
|
+
diagnostics.swallowed("ios.StatusBarHandler._apply")
|
|
3351
3910
|
|
|
3352
3911
|
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
3353
3912
|
return
|
|
@@ -3413,7 +3972,7 @@ def _ensure_keyboard_observer() -> None:
|
|
|
3413
3972
|
None,
|
|
3414
3973
|
)
|
|
3415
3974
|
except Exception:
|
|
3416
|
-
|
|
3975
|
+
diagnostics.swallowed("ios._ensure_keyboard_observer")
|
|
3417
3976
|
|
|
3418
3977
|
|
|
3419
3978
|
class KeyboardAvoidingViewHandler(IOSViewHandler):
|
|
@@ -3429,9 +3988,7 @@ class KeyboardAvoidingViewHandler(IOSViewHandler):
|
|
|
3429
3988
|
|
|
3430
3989
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
3431
3990
|
_ensure_keyboard_observer()
|
|
3432
|
-
|
|
3433
|
-
v.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
3434
|
-
return v
|
|
3991
|
+
return _new_container_view()
|
|
3435
3992
|
|
|
3436
3993
|
|
|
3437
3994
|
# ======================================================================
|
|
@@ -3457,7 +4014,7 @@ def _tabbar_did_select_imp(self_ptr: int, cmd_ptr: int, tabbar_ptr: int, item_pt
|
|
|
3457
4014
|
if 0 <= index < len(items):
|
|
3458
4015
|
_fire_ptr(int(tabbar_ptr), "on_tab_select", items[index].get("name", ""))
|
|
3459
4016
|
except Exception:
|
|
3460
|
-
|
|
4017
|
+
diagnostics.swallowed("ios._tabbar_did_select_imp")
|
|
3461
4018
|
|
|
3462
4019
|
|
|
3463
4020
|
_tabbar_imp_ref = _DELEGATE_IMP_TYPE(_tabbar_did_select_imp)
|
|
@@ -3543,7 +4100,7 @@ class TabBarHandler(IOSViewHandler):
|
|
|
3543
4100
|
try:
|
|
3544
4101
|
tab_bar.setItems_animated_(bar_items, False)
|
|
3545
4102
|
except Exception:
|
|
3546
|
-
|
|
4103
|
+
diagnostics.swallowed("ios.TabBarHandler._set_bar_items")
|
|
3547
4104
|
|
|
3548
4105
|
def _resolve_icon(self, UIImage: Any, icon: Any) -> Any:
|
|
3549
4106
|
"""Resolve a tab icon spec to a UIImage, or return None.
|
|
@@ -3577,7 +4134,7 @@ class TabBarHandler(IOSViewHandler):
|
|
|
3577
4134
|
if i < len(all_items):
|
|
3578
4135
|
tab_bar.setSelectedItem_(all_items[i])
|
|
3579
4136
|
except Exception:
|
|
3580
|
-
|
|
4137
|
+
diagnostics.swallowed("ios.TabBarHandler._set_active")
|
|
3581
4138
|
break
|
|
3582
4139
|
|
|
3583
4140
|
|
|
@@ -3600,7 +4157,7 @@ def _top_view_controller_for_alert(app: Any) -> Any:
|
|
|
3600
4157
|
try:
|
|
3601
4158
|
window = app.keyWindow
|
|
3602
4159
|
except Exception:
|
|
3603
|
-
|
|
4160
|
+
diagnostics.swallowed("ios._top_view_controller_for_alert")
|
|
3604
4161
|
|
|
3605
4162
|
if window is None:
|
|
3606
4163
|
try:
|
|
@@ -3612,7 +4169,7 @@ def _top_view_controller_for_alert(app: Any) -> Any:
|
|
|
3612
4169
|
if window is None and windows:
|
|
3613
4170
|
window = windows[0]
|
|
3614
4171
|
except Exception:
|
|
3615
|
-
|
|
4172
|
+
diagnostics.swallowed("ios._top_view_controller_for_alert")
|
|
3616
4173
|
|
|
3617
4174
|
if window is None:
|
|
3618
4175
|
return None
|
|
@@ -3629,7 +4186,7 @@ def _top_view_controller_for_alert(app: Any) -> Any:
|
|
|
3629
4186
|
if visible is not None:
|
|
3630
4187
|
top = visible
|
|
3631
4188
|
except Exception:
|
|
3632
|
-
|
|
4189
|
+
diagnostics.swallowed("ios._top_view_controller_for_alert")
|
|
3633
4190
|
|
|
3634
4191
|
while top is not None:
|
|
3635
4192
|
try:
|
|
@@ -3677,7 +4234,7 @@ def _present_alert(
|
|
|
3677
4234
|
try:
|
|
3678
4235
|
on_result(index)
|
|
3679
4236
|
except Exception:
|
|
3680
|
-
|
|
4237
|
+
diagnostics.swallowed("ios._present_alert._deliver")
|
|
3681
4238
|
|
|
3682
4239
|
def _present_on_main() -> None:
|
|
3683
4240
|
try:
|
|
@@ -3778,7 +4335,7 @@ class PickerHandler(IOSViewHandler):
|
|
|
3778
4335
|
try:
|
|
3779
4336
|
btn.setTitle_forState_(_picker_button_title(merged), 0)
|
|
3780
4337
|
except Exception:
|
|
3781
|
-
|
|
4338
|
+
diagnostics.swallowed("ios.PickerHandler._apply")
|
|
3782
4339
|
_apply_accessibility(btn, props)
|
|
3783
4340
|
|
|
3784
4341
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
@@ -3814,10 +4371,10 @@ def _checkbox_set_image(btn: Any) -> None:
|
|
|
3814
4371
|
if tinted is not None:
|
|
3815
4372
|
image = tinted
|
|
3816
4373
|
except Exception:
|
|
3817
|
-
|
|
4374
|
+
diagnostics.swallowed("ios._checkbox_set_image")
|
|
3818
4375
|
btn.setImage_forState_(image, 0)
|
|
3819
4376
|
except Exception:
|
|
3820
|
-
|
|
4377
|
+
diagnostics.swallowed("ios._checkbox_set_image")
|
|
3821
4378
|
|
|
3822
4379
|
|
|
3823
4380
|
def _checkbox_toggle(btn: Any) -> None:
|
|
@@ -3855,7 +4412,7 @@ class CheckboxHandler(IOSViewHandler):
|
|
|
3855
4412
|
btn.setTitleColor_forState_(_uicolor("#111111"), 0)
|
|
3856
4413
|
btn.setTintColor_(_uicolor("#111111"))
|
|
3857
4414
|
except Exception:
|
|
3858
|
-
|
|
4415
|
+
diagnostics.swallowed("ios.CheckboxHandler._apply")
|
|
3859
4416
|
if "value" in props:
|
|
3860
4417
|
state["value"] = bool(props["value"])
|
|
3861
4418
|
if "label" in props:
|
|
@@ -3863,14 +4420,14 @@ class CheckboxHandler(IOSViewHandler):
|
|
|
3863
4420
|
try:
|
|
3864
4421
|
btn.setTitle_forState_(str(label) if label is not None else "", 0)
|
|
3865
4422
|
except Exception:
|
|
3866
|
-
|
|
4423
|
+
diagnostics.swallowed("ios.CheckboxHandler._apply")
|
|
3867
4424
|
# An image-bearing custom button is not exposed to the
|
|
3868
4425
|
# accessibility tree by title alone; mirror the label
|
|
3869
4426
|
# explicitly (an accessibility_label prop still wins below).
|
|
3870
4427
|
try:
|
|
3871
4428
|
btn.setAccessibilityLabel_(str(label) if label is not None else "")
|
|
3872
4429
|
except Exception:
|
|
3873
|
-
|
|
4430
|
+
diagnostics.swallowed("ios.CheckboxHandler._apply")
|
|
3874
4431
|
if "disabled" in props:
|
|
3875
4432
|
# ``disabled`` is present only when True; a removed prop
|
|
3876
4433
|
# (None) re-enables the control.
|
|
@@ -3878,7 +4435,7 @@ class CheckboxHandler(IOSViewHandler):
|
|
|
3878
4435
|
try:
|
|
3879
4436
|
btn.setEnabled_(not disabled)
|
|
3880
4437
|
except Exception:
|
|
3881
|
-
|
|
4438
|
+
diagnostics.swallowed("ios.CheckboxHandler._apply")
|
|
3882
4439
|
_checkbox_set_image(btn)
|
|
3883
4440
|
_apply_accessibility(btn, props)
|
|
3884
4441
|
|
|
@@ -3935,7 +4492,7 @@ class SegmentedControlHandler(IOSViewHandler):
|
|
|
3935
4492
|
for i, title in enumerate(new_segments):
|
|
3936
4493
|
control.insertSegmentWithTitle_atIndex_animated_(title, i, False)
|
|
3937
4494
|
except Exception:
|
|
3938
|
-
|
|
4495
|
+
diagnostics.swallowed("ios.SegmentedControlHandler._apply")
|
|
3939
4496
|
finally:
|
|
3940
4497
|
state["suppress"] = False
|
|
3941
4498
|
rebuilt = True
|
|
@@ -3948,7 +4505,7 @@ class SegmentedControlHandler(IOSViewHandler):
|
|
|
3948
4505
|
try:
|
|
3949
4506
|
control.setSelectedSegmentIndex_(int(merged.get("selected_index", 0) or 0))
|
|
3950
4507
|
except Exception:
|
|
3951
|
-
|
|
4508
|
+
diagnostics.swallowed("ios.SegmentedControlHandler._apply")
|
|
3952
4509
|
finally:
|
|
3953
4510
|
state["suppress"] = False
|
|
3954
4511
|
if "tint_color" in props and props["tint_color"] is not None:
|
|
@@ -3956,11 +4513,11 @@ class SegmentedControlHandler(IOSViewHandler):
|
|
|
3956
4513
|
try:
|
|
3957
4514
|
control.setSelectedSegmentTintColor_(color) # iOS 13+
|
|
3958
4515
|
except Exception:
|
|
3959
|
-
|
|
4516
|
+
diagnostics.swallowed("ios.SegmentedControlHandler._apply")
|
|
3960
4517
|
try:
|
|
3961
4518
|
control.setTintColor_(color)
|
|
3962
4519
|
except Exception:
|
|
3963
|
-
|
|
4520
|
+
diagnostics.swallowed("ios.SegmentedControlHandler._apply")
|
|
3964
4521
|
if "enabled" in props:
|
|
3965
4522
|
# ``enabled`` is present only when False; a removed prop
|
|
3966
4523
|
# (None) re-enables the control.
|
|
@@ -3968,7 +4525,7 @@ class SegmentedControlHandler(IOSViewHandler):
|
|
|
3968
4525
|
try:
|
|
3969
4526
|
control.setEnabled_(True if enabled is None else bool(enabled))
|
|
3970
4527
|
except Exception:
|
|
3971
|
-
|
|
4528
|
+
diagnostics.swallowed("ios.SegmentedControlHandler._apply")
|
|
3972
4529
|
_apply_accessibility(control, props)
|
|
3973
4530
|
|
|
3974
4531
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
@@ -4007,7 +4564,7 @@ def _date_formatter(mode: str) -> Any:
|
|
|
4007
4564
|
NSLocale = ObjCClass("NSLocale")
|
|
4008
4565
|
formatter.setLocale_(NSLocale.alloc().initWithLocaleIdentifier_("en_US_POSIX"))
|
|
4009
4566
|
except Exception:
|
|
4010
|
-
|
|
4567
|
+
diagnostics.swallowed("ios._date_formatter")
|
|
4011
4568
|
formatter.retain()
|
|
4012
4569
|
_pn_date_formatters[fmt] = formatter
|
|
4013
4570
|
return formatter
|
|
@@ -4024,7 +4581,7 @@ class DatePickerHandler(IOSViewHandler):
|
|
|
4024
4581
|
try:
|
|
4025
4582
|
picker.setPreferredDatePickerStyle_(2) # UIDatePickerStyleCompact
|
|
4026
4583
|
except Exception:
|
|
4027
|
-
|
|
4584
|
+
diagnostics.swallowed("ios.DatePickerHandler._build")
|
|
4028
4585
|
|
|
4029
4586
|
def _on_change() -> None:
|
|
4030
4587
|
state = _state_of(picker)
|
|
@@ -4049,7 +4606,7 @@ class DatePickerHandler(IOSViewHandler):
|
|
|
4049
4606
|
try:
|
|
4050
4607
|
picker.setDatePickerMode_(mode_map.get(mode, 1))
|
|
4051
4608
|
except Exception:
|
|
4052
|
-
|
|
4609
|
+
diagnostics.swallowed("ios.DatePickerHandler._apply")
|
|
4053
4610
|
if "minimum" in props:
|
|
4054
4611
|
self._set_bound(picker, "setMinimumDate_", props["minimum"], mode)
|
|
4055
4612
|
if "maximum" in props:
|
|
@@ -4064,7 +4621,7 @@ class DatePickerHandler(IOSViewHandler):
|
|
|
4064
4621
|
try:
|
|
4065
4622
|
picker.setDate_animated_(date, False)
|
|
4066
4623
|
except Exception:
|
|
4067
|
-
|
|
4624
|
+
diagnostics.swallowed("ios.DatePickerHandler._apply")
|
|
4068
4625
|
finally:
|
|
4069
4626
|
state["suppress"] = False
|
|
4070
4627
|
if "enabled" in props:
|
|
@@ -4074,7 +4631,7 @@ class DatePickerHandler(IOSViewHandler):
|
|
|
4074
4631
|
try:
|
|
4075
4632
|
picker.setEnabled_(True if enabled is None else bool(enabled))
|
|
4076
4633
|
except Exception:
|
|
4077
|
-
|
|
4634
|
+
diagnostics.swallowed("ios.DatePickerHandler._apply")
|
|
4078
4635
|
_apply_accessibility(picker, props)
|
|
4079
4636
|
|
|
4080
4637
|
def _set_bound(self, picker: Any, selector: str, value: Any, mode: str) -> None:
|
|
@@ -4085,7 +4642,7 @@ class DatePickerHandler(IOSViewHandler):
|
|
|
4085
4642
|
date = _date_formatter(mode).dateFromString_(str(value))
|
|
4086
4643
|
getattr(picker, selector)(date)
|
|
4087
4644
|
except Exception:
|
|
4088
|
-
|
|
4645
|
+
diagnostics.swallowed("ios.DatePickerHandler._set_bound")
|
|
4089
4646
|
|
|
4090
4647
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
4091
4648
|
try:
|
|
@@ -4230,7 +4787,7 @@ def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> in
|
|
|
4230
4787
|
cell.setFrame_(((0, 0), (cell_w, row_h)))
|
|
4231
4788
|
cell.contentView.setFrame_(((0, 0), (cell_w, row_h)))
|
|
4232
4789
|
except Exception:
|
|
4233
|
-
|
|
4790
|
+
diagnostics.swallowed("ios._table_cell_imp")
|
|
4234
4791
|
|
|
4235
4792
|
content = cell.contentView
|
|
4236
4793
|
try:
|
|
@@ -4240,7 +4797,7 @@ def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> in
|
|
|
4240
4797
|
for sub in list(existing_subs or []):
|
|
4241
4798
|
sub.removeFromSuperview()
|
|
4242
4799
|
except Exception:
|
|
4243
|
-
|
|
4800
|
+
diagnostics.swallowed("ios._table_cell_imp")
|
|
4244
4801
|
|
|
4245
4802
|
# Only rows in (or near) the visible content window get a
|
|
4246
4803
|
# mounted subtree. UITableView's accessibility container calls
|
|
@@ -4270,7 +4827,7 @@ def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> in
|
|
|
4270
4827
|
try:
|
|
4271
4828
|
cell.setAccessibilityElementsHidden_(not in_window)
|
|
4272
4829
|
except Exception:
|
|
4273
|
-
|
|
4830
|
+
diagnostics.swallowed("ios._table_cell_imp")
|
|
4274
4831
|
|
|
4275
4832
|
if in_window and info is not None:
|
|
4276
4833
|
render_row = info.get("render_row")
|
|
@@ -4289,7 +4846,7 @@ def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> in
|
|
|
4289
4846
|
try:
|
|
4290
4847
|
root.setFrame_(((0, 0), (cell_w, row_h)))
|
|
4291
4848
|
except Exception:
|
|
4292
|
-
|
|
4849
|
+
diagnostics.swallowed("ios._table_cell_imp")
|
|
4293
4850
|
except Exception:
|
|
4294
4851
|
print(f"[VirtualList][iOS] mount for row={row} raised:")
|
|
4295
4852
|
_tb.print_exc()
|
|
@@ -4314,7 +4871,7 @@ def _table_did_select_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int)
|
|
|
4314
4871
|
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_bool]
|
|
4315
4872
|
_objc_msgSend(_ct.c_void_p(tv_ptr), _SEL_DESELECT_ROW, _ct.c_void_p(ip_ptr), True)
|
|
4316
4873
|
except Exception:
|
|
4317
|
-
|
|
4874
|
+
diagnostics.swallowed("ios._table_did_select_imp")
|
|
4318
4875
|
_fire_ptr(int(tv_ptr or 0), "on_row_press", row)
|
|
4319
4876
|
|
|
4320
4877
|
|
|
@@ -4440,7 +4997,7 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4440
4997
|
estimate = float(props.get("row_height", 44.0) or 44.0)
|
|
4441
4998
|
tv.setEstimatedRowHeight_(max(1.0, estimate))
|
|
4442
4999
|
except Exception:
|
|
4443
|
-
|
|
5000
|
+
diagnostics.swallowed("ios.VirtualListHandler._build")
|
|
4444
5001
|
tv.retain()
|
|
4445
5002
|
_pn_retained_views.append(tv)
|
|
4446
5003
|
|
|
@@ -4498,12 +5055,12 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4498
5055
|
try:
|
|
4499
5056
|
tv.setShowsVerticalScrollIndicator_(visible)
|
|
4500
5057
|
except Exception:
|
|
4501
|
-
|
|
5058
|
+
diagnostics.swallowed("ios.VirtualListHandler._apply")
|
|
4502
5059
|
if data_changed and not initial:
|
|
4503
5060
|
try:
|
|
4504
5061
|
tv.reloadData()
|
|
4505
5062
|
except Exception:
|
|
4506
|
-
|
|
5063
|
+
diagnostics.swallowed("ios.VirtualListHandler._apply")
|
|
4507
5064
|
|
|
4508
5065
|
def _teardown(self, tv: Any) -> None:
|
|
4509
5066
|
info = self._source_state(tv)
|
|
@@ -4512,7 +5069,7 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4512
5069
|
try:
|
|
4513
5070
|
info["pool"].release_all()
|
|
4514
5071
|
except Exception:
|
|
4515
|
-
|
|
5072
|
+
diagnostics.swallowed("ios.VirtualListHandler._teardown")
|
|
4516
5073
|
info["tv"] = None
|
|
4517
5074
|
_pn_table_state.pop(info.get("source_ptr"), None)
|
|
4518
5075
|
|
|
@@ -4536,7 +5093,7 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4536
5093
|
try:
|
|
4537
5094
|
native_view.setContentOffset_animated_((0.0, y), animated)
|
|
4538
5095
|
except Exception:
|
|
4539
|
-
|
|
5096
|
+
diagnostics.swallowed("ios.VirtualListHandler.command")
|
|
4540
5097
|
return None
|
|
4541
5098
|
if name == "scroll_to_index":
|
|
4542
5099
|
index = int(args.get("index", 0) or 0)
|
|
@@ -4549,7 +5106,7 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4549
5106
|
# 1 = UITableViewScrollPositionTop
|
|
4550
5107
|
native_view.scrollToRowAtIndexPath_atScrollPosition_animated_(path, 1, animated)
|
|
4551
5108
|
except Exception:
|
|
4552
|
-
|
|
5109
|
+
diagnostics.swallowed("ios.VirtualListHandler.command")
|
|
4553
5110
|
return None
|
|
4554
5111
|
if name == "scroll_to_end":
|
|
4555
5112
|
animated = args.get("animated", True) is not False
|
|
@@ -4559,7 +5116,7 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4559
5116
|
target = max(0.0, content_h - bounds_h)
|
|
4560
5117
|
native_view.setContentOffset_animated_((0.0, target), animated)
|
|
4561
5118
|
except Exception:
|
|
4562
|
-
|
|
5119
|
+
diagnostics.swallowed("ios.VirtualListHandler.command")
|
|
4563
5120
|
return None
|
|
4564
5121
|
if name == "get_scroll_offset":
|
|
4565
5122
|
try:
|