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
|
@@ -36,15 +36,45 @@ parent-relative frames render correctly without reparenting.
|
|
|
36
36
|
ScrollViews shift their children's placement by the current scroll
|
|
37
37
|
offset, which yields real wheel scrolling in the preview.
|
|
38
38
|
|
|
39
|
+
Interaction and stacking props
|
|
40
|
+
------------------------------
|
|
41
|
+
``z_index`` is honored among logical siblings: whenever a sibling set
|
|
42
|
+
contains an explicit ``z_index``, the parent's children are re-lifted
|
|
43
|
+
in ascending ``z_index`` order (missing values count as 0, ties keep
|
|
44
|
+
insertion order), so higher values render above their siblings.
|
|
45
|
+
``pointer_events`` is approximated by suspending a widget's Tk binding
|
|
46
|
+
tags: ``"none"`` mutes the view and its descendants, ``"box_none"``
|
|
47
|
+
mutes only the view itself, and ``"box_only"`` mutes only the
|
|
48
|
+
descendants. Suspension is reversible (the original tags are restored
|
|
49
|
+
when the prop returns to ``"auto"``), but it is coarser than the real
|
|
50
|
+
platforms: all bindings go quiet (keyboard included), and muted
|
|
51
|
+
widgets drop events rather than passing them through to whatever sits
|
|
52
|
+
underneath. ``hit_slop`` can't grow the initial press target (Tk
|
|
53
|
+
hit-tests presses strictly by widget bounds), but it does expand press
|
|
54
|
+
*tracking*: once a press starts, Tk's implicit grab keeps streaming
|
|
55
|
+
motion and release events to the pressed widget, and ``Pressable``
|
|
56
|
+
treats positions within the slop insets of its frame as still inside
|
|
57
|
+
when deciding whether a release fires ``on_press`` or a pending long
|
|
58
|
+
press survives pointer drift.
|
|
59
|
+
|
|
39
60
|
Scope
|
|
40
61
|
-----
|
|
41
62
|
This is a **preview** backend, not a production desktop target. It
|
|
42
63
|
favors fidelity of layout and behavior over pixel-perfect chrome:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
``
|
|
64
|
+
shadows, per-widget opacity, and overflow clipping are approximated or
|
|
65
|
+
omitted (Tkinter can't express them cheaply). Rounded corners
|
|
66
|
+
(``border_radius`` plus the per-corner ``border_*_radius`` keys, which
|
|
67
|
+
fall back to it) are approximated on Frame-based views by painting the
|
|
68
|
+
background and a uniform border onto a covering ``Canvas``; the corner
|
|
69
|
+
cutouts are filled with the parent's solid background rather than
|
|
70
|
+
being truly transparent, and content leaves (Text, Button, Image)
|
|
71
|
+
ignore radius entirely. Native animation is declined
|
|
72
|
+
(``start_animation`` returns ``False``), so the Python ticker drives
|
|
73
|
+
previews of animations through ``set_animated_property``: translation
|
|
74
|
+
maps onto placement, animated ``background_color`` and ``color``
|
|
75
|
+
(including interpolated ``"#AARRGGBB"`` strings) map onto ``configure``
|
|
76
|
+
(or the rounded-corner canvas when one is active), and rotate, scale,
|
|
77
|
+
and opacity frames are silently ignored.
|
|
48
78
|
|
|
49
79
|
This module imports ``tkinter`` at import time, so it is only imported
|
|
50
80
|
when ``PN_PLATFORM=desktop``. Off-device unit tests inject a mock
|
|
@@ -54,6 +84,7 @@ and never trigger this path.
|
|
|
54
84
|
|
|
55
85
|
from __future__ import annotations
|
|
56
86
|
|
|
87
|
+
import bisect
|
|
57
88
|
import math
|
|
58
89
|
import re
|
|
59
90
|
import time
|
|
@@ -62,6 +93,7 @@ from tkinter import font as tkfont
|
|
|
62
93
|
from tkinter import ttk
|
|
63
94
|
from typing import Any, Dict, List, Optional, Tuple
|
|
64
95
|
|
|
96
|
+
from .. import diagnostics
|
|
65
97
|
from ..events import dispatch_event, event_names
|
|
66
98
|
from ..gestures import make_arbiter
|
|
67
99
|
from .base import ViewHandler
|
|
@@ -325,7 +357,17 @@ def _place(widget: Any) -> None:
|
|
|
325
357
|
widget.place(in_=target, x=x + tx - sx, y=y + ty - sy, width=max(0.0, w), height=max(0.0, h))
|
|
326
358
|
widget.lift()
|
|
327
359
|
except Exception:
|
|
328
|
-
|
|
360
|
+
diagnostics.swallowed("desktop._place")
|
|
361
|
+
return
|
|
362
|
+
# The unconditional lift above would put a low ``z_index`` sibling
|
|
363
|
+
# on top, so restack whenever this sibling set uses ``z_index``.
|
|
364
|
+
if parent is not None and getattr(parent, "_pn_has_z", False):
|
|
365
|
+
_restack(parent)
|
|
366
|
+
# Frames are the only reliable resize signal for unmapped widgets,
|
|
367
|
+
# so keep the rounded background in sync here (cheap when the
|
|
368
|
+
# paint signature is unchanged).
|
|
369
|
+
if getattr(widget, "_pn_radius_canvas", None) is not None:
|
|
370
|
+
_redraw_rounded_background(widget)
|
|
329
371
|
|
|
330
372
|
|
|
331
373
|
def _register_child(parent: Any, child: Any, index: int) -> None:
|
|
@@ -346,6 +388,369 @@ def _unregister_child(parent: Any, child: Any) -> None:
|
|
|
346
388
|
parent._pn_children = children
|
|
347
389
|
|
|
348
390
|
|
|
391
|
+
def _z_index(widget: Any) -> float:
|
|
392
|
+
"""Return the widget's ``z_index`` (missing or unparseable is 0)."""
|
|
393
|
+
merged = getattr(widget, "_pn_props", None) or {}
|
|
394
|
+
z = merged.get("z_index")
|
|
395
|
+
if z is None:
|
|
396
|
+
return 0.0
|
|
397
|
+
try:
|
|
398
|
+
return float(z)
|
|
399
|
+
except (TypeError, ValueError):
|
|
400
|
+
return 0.0
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _lift_subtree(widget: Any) -> None:
|
|
404
|
+
"""Lift ``widget``, then its logical descendants in ``z_index`` order.
|
|
405
|
+
|
|
406
|
+
Every desktop widget is a Tk child of the shared stage, so
|
|
407
|
+
stacking is one global order: lifting a container doesn't carry
|
|
408
|
+
its subtree along. Pre-order lifting keeps each subtree above its
|
|
409
|
+
root and applies nested ``z_index`` ordering along the way.
|
|
410
|
+
"""
|
|
411
|
+
try:
|
|
412
|
+
widget.lift()
|
|
413
|
+
except Exception:
|
|
414
|
+
diagnostics.swallowed("desktop._lift_subtree")
|
|
415
|
+
for child in sorted(getattr(widget, "_pn_children", None) or [], key=_z_index):
|
|
416
|
+
_lift_subtree(child)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _restack(parent: Any) -> None:
|
|
420
|
+
"""Re-lift ``parent``'s children (and their subtrees) by ``z_index``.
|
|
421
|
+
|
|
422
|
+
``sorted`` is stable, so siblings with equal ``z_index`` keep their
|
|
423
|
+
insertion order. Called whenever a sibling set contains an explicit
|
|
424
|
+
``z_index`` (see ``_place`` and ``_apply_common``).
|
|
425
|
+
"""
|
|
426
|
+
for child in sorted(getattr(parent, "_pn_children", None) or [], key=_z_index):
|
|
427
|
+
_lift_subtree(child)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ----------------------------------------------------------------------
|
|
431
|
+
# ``pointer_events`` (best-effort via Tk binding tags)
|
|
432
|
+
# ----------------------------------------------------------------------
|
|
433
|
+
#
|
|
434
|
+
# A muted widget gets a single dummy binding tag with no bindings, so
|
|
435
|
+
# no event scripts (its own, its class's, or the global "all" tag's)
|
|
436
|
+
# run while the mode is active; the original tags are saved and
|
|
437
|
+
# restored when the prop returns to "auto". This is coarser than the
|
|
438
|
+
# real platforms: keyboard bindings go quiet too, and Tk still routes
|
|
439
|
+
# the event to the widget under the cursor, so muted widgets drop
|
|
440
|
+
# events instead of letting them fall through to lower widgets.
|
|
441
|
+
|
|
442
|
+
_PE_MUTED_TAG = "pn_pointer_events_off"
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _pointer_mode(widget: Any) -> str:
|
|
446
|
+
"""Return the widget's own ``pointer_events`` mode (default ``"auto"``)."""
|
|
447
|
+
merged = getattr(widget, "_pn_props", None) or {}
|
|
448
|
+
mode = merged.get("pointer_events")
|
|
449
|
+
return mode if mode in ("none", "box_none", "box_only") else "auto"
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _pointer_muted(widget: Any) -> bool:
|
|
453
|
+
"""Whether the widget should ignore pointer events right now.
|
|
454
|
+
|
|
455
|
+
True when its own mode mutes the view (``"none"`` / ``"box_none"``)
|
|
456
|
+
or when any logical ancestor mutes its subtree (``"none"`` /
|
|
457
|
+
``"box_only"``).
|
|
458
|
+
"""
|
|
459
|
+
if _pointer_mode(widget) in ("none", "box_none"):
|
|
460
|
+
return True
|
|
461
|
+
node = getattr(widget, "_pn_parent", None)
|
|
462
|
+
while node is not None:
|
|
463
|
+
if _pointer_mode(node) in ("none", "box_only"):
|
|
464
|
+
return True
|
|
465
|
+
node = getattr(node, "_pn_parent", None)
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _set_pointer_muted(widget: Any, muted: bool) -> None:
|
|
470
|
+
"""Suspend or restore the widget's Tk binding tags (idempotent)."""
|
|
471
|
+
try:
|
|
472
|
+
current = tuple(widget.bindtags())
|
|
473
|
+
if muted:
|
|
474
|
+
if current != (_PE_MUTED_TAG,):
|
|
475
|
+
widget._pn_saved_bindtags = current
|
|
476
|
+
widget.bindtags((_PE_MUTED_TAG,))
|
|
477
|
+
elif current == (_PE_MUTED_TAG,):
|
|
478
|
+
saved = getattr(widget, "_pn_saved_bindtags", None)
|
|
479
|
+
if saved:
|
|
480
|
+
widget.bindtags(saved)
|
|
481
|
+
except Exception:
|
|
482
|
+
diagnostics.swallowed("desktop._set_pointer_muted")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _refresh_pointer_events(widget: Any) -> None:
|
|
486
|
+
"""Recompute pointer muting for ``widget`` and its logical subtree."""
|
|
487
|
+
muted = _pointer_muted(widget)
|
|
488
|
+
_set_pointer_muted(widget, muted)
|
|
489
|
+
canvas = getattr(widget, "_pn_radius_canvas", None)
|
|
490
|
+
if canvas is not None:
|
|
491
|
+
# The rounded-corner canvas covers the widget and forwards
|
|
492
|
+
# events to it (see ``_set_rounded_background``), so it mutes
|
|
493
|
+
# exactly when its host does.
|
|
494
|
+
_set_pointer_muted(canvas, muted)
|
|
495
|
+
for child in getattr(widget, "_pn_children", None) or []:
|
|
496
|
+
_refresh_pointer_events(child)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# ----------------------------------------------------------------------
|
|
500
|
+
# ``hit_slop`` (press-tracking expansion)
|
|
501
|
+
# ----------------------------------------------------------------------
|
|
502
|
+
#
|
|
503
|
+
# Tk delivers the initial press only to the widget under the cursor,
|
|
504
|
+
# so the slop insets can't grow the press target the way they do on
|
|
505
|
+
# device. They do expand the *tracking* rect: Tk's implicit grab keeps
|
|
506
|
+
# streaming motion and release events to the pressed widget even past
|
|
507
|
+
# its edges, and ``_within_hit_rect`` treats positions within the slop
|
|
508
|
+
# insets of the widget's frame as inside.
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _parse_hit_slop(value: Any) -> Tuple[float, float, float, float]:
|
|
512
|
+
"""Normalize a ``hit_slop`` prop into ``(top, left, bottom, right)`` insets.
|
|
513
|
+
|
|
514
|
+
Accepts a uniform number or a dict with any of ``top`` / ``left``
|
|
515
|
+
/ ``bottom`` / ``right``; missing or unparseable entries are 0.
|
|
516
|
+
"""
|
|
517
|
+
if isinstance(value, dict):
|
|
518
|
+
return (
|
|
519
|
+
max(0.0, _finite(value.get("top"))),
|
|
520
|
+
max(0.0, _finite(value.get("left"))),
|
|
521
|
+
max(0.0, _finite(value.get("bottom"))),
|
|
522
|
+
max(0.0, _finite(value.get("right"))),
|
|
523
|
+
)
|
|
524
|
+
uniform = max(0.0, _finite(value))
|
|
525
|
+
return (uniform, uniform, uniform, uniform)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _within_hit_rect(widget: Any, x: float, y: float) -> bool:
|
|
529
|
+
"""Whether a widget-relative point is inside the frame plus ``hit_slop``.
|
|
530
|
+
|
|
531
|
+
Falls back to the live Tk size when the layout frame isn't known
|
|
532
|
+
yet, and to "inside" when neither is available (never drop a press
|
|
533
|
+
for lack of geometry).
|
|
534
|
+
"""
|
|
535
|
+
frame = getattr(widget, "_pn_frame", None)
|
|
536
|
+
if frame is not None:
|
|
537
|
+
w, h = frame[2], frame[3]
|
|
538
|
+
else:
|
|
539
|
+
try:
|
|
540
|
+
w, h = float(widget.winfo_width()), float(widget.winfo_height())
|
|
541
|
+
except Exception:
|
|
542
|
+
return True
|
|
543
|
+
top, left, bottom, right = getattr(widget, "_pn_hit_slop", (0.0, 0.0, 0.0, 0.0))
|
|
544
|
+
return (-left <= x <= w + right) and (-top <= y <= h + bottom)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
# ----------------------------------------------------------------------
|
|
548
|
+
# Rounded corners (Canvas approximation)
|
|
549
|
+
# ----------------------------------------------------------------------
|
|
550
|
+
#
|
|
551
|
+
# Tk widgets are rectangles and can't be clipped, so Frame-based views
|
|
552
|
+
# with a border radius get a full-size Canvas child that paints the
|
|
553
|
+
# background (and a uniform border outline) as a rounded polygon. The
|
|
554
|
+
# corner cutouts can't be transparent; they're filled with the logical
|
|
555
|
+
# parent's background color, which reads correctly whenever the parent
|
|
556
|
+
# paints a solid color behind the view. Content leaves (Text, Button,
|
|
557
|
+
# Image) skip the approximation entirely: a covering canvas would hide
|
|
558
|
+
# what they draw.
|
|
559
|
+
|
|
560
|
+
_RADIUS_ARC_STEPS = 8
|
|
561
|
+
|
|
562
|
+
_SIDE_BORDER_WIDTH_KEYS = (
|
|
563
|
+
"border_left_width",
|
|
564
|
+
"border_top_width",
|
|
565
|
+
"border_right_width",
|
|
566
|
+
"border_bottom_width",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _corner_radii(props: Dict[str, Any]) -> Tuple[float, float, float, float]:
|
|
571
|
+
"""Resolve ``(top-left, top-right, bottom-right, bottom-left)`` radii.
|
|
572
|
+
|
|
573
|
+
Per-corner keys fall back to ``border_radius``; missing values are
|
|
574
|
+
0. Negative or unparseable values clamp to 0.
|
|
575
|
+
"""
|
|
576
|
+
base = max(0.0, _finite(props.get("border_radius")))
|
|
577
|
+
|
|
578
|
+
def _one(key: str) -> float:
|
|
579
|
+
value = props.get(key)
|
|
580
|
+
if value is None:
|
|
581
|
+
return base
|
|
582
|
+
return max(0.0, _finite(value, base))
|
|
583
|
+
|
|
584
|
+
return (
|
|
585
|
+
_one("border_top_left_radius"),
|
|
586
|
+
_one("border_top_right_radius"),
|
|
587
|
+
_one("border_bottom_right_radius"),
|
|
588
|
+
_one("border_bottom_left_radius"),
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _resolve_border(props: Dict[str, Any]) -> Tuple[float, str]:
|
|
593
|
+
"""Resolve the uniform ``(width, color)`` border approximation.
|
|
594
|
+
|
|
595
|
+
Tk borders are uniform, so per-side borders collapse to the widest
|
|
596
|
+
side's width and the first nonzero side's color.
|
|
597
|
+
"""
|
|
598
|
+
width = props.get("border_width")
|
|
599
|
+
color = _tk_color(props.get("border_color")) or "#3c3c43"
|
|
600
|
+
side_widths = [props.get(k) for k in _SIDE_BORDER_WIDTH_KEYS]
|
|
601
|
+
if any(w is not None for w in side_widths):
|
|
602
|
+
width = max(float(w) for w in side_widths if w is not None)
|
|
603
|
+
for side, w in zip(("left", "top", "right", "bottom"), side_widths):
|
|
604
|
+
if w is not None and float(w) > 0:
|
|
605
|
+
side_color = _tk_color(props.get(f"border_{side}_color"))
|
|
606
|
+
if side_color is not None:
|
|
607
|
+
color = side_color
|
|
608
|
+
break
|
|
609
|
+
return (max(0.0, _finite(width)), color)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _underlay_color(widget: Any) -> str:
|
|
613
|
+
"""Best-effort color of whatever paints behind ``widget``.
|
|
614
|
+
|
|
615
|
+
Used to fill the corner cutouts of a rounded view: the logical
|
|
616
|
+
parent's rounded fill when it has one, else its plain Tk
|
|
617
|
+
background. A solid guess is the documented limitation; gradients
|
|
618
|
+
or images behind a rounded view can't be matched.
|
|
619
|
+
"""
|
|
620
|
+
parent = getattr(widget, "_pn_parent", None)
|
|
621
|
+
target = parent if parent is not None else get_root_container()
|
|
622
|
+
if target is not None:
|
|
623
|
+
state = getattr(target, "_pn_radius_state", None)
|
|
624
|
+
if state is not None and state.get("fill"):
|
|
625
|
+
return str(state["fill"])
|
|
626
|
+
try:
|
|
627
|
+
return str(target.cget("background"))
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
return "#ffffff"
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _rounded_rect_points(w: float, h: float, radii: Tuple[float, float, float, float]) -> List[float]:
|
|
634
|
+
"""Vertices of a rounded rectangle, corner arcs sampled clockwise.
|
|
635
|
+
|
|
636
|
+
Radii are clamped proportionally so adjacent corners never
|
|
637
|
+
overlap. A zero radius degenerates to the plain corner point.
|
|
638
|
+
"""
|
|
639
|
+
tl, tr, br, bl = radii
|
|
640
|
+
scale = min(
|
|
641
|
+
1.0,
|
|
642
|
+
w / max(1e-6, tl + tr),
|
|
643
|
+
w / max(1e-6, bl + br),
|
|
644
|
+
h / max(1e-6, tl + bl),
|
|
645
|
+
h / max(1e-6, tr + br),
|
|
646
|
+
)
|
|
647
|
+
tl, tr, br, bl = (r * scale for r in (tl, tr, br, bl))
|
|
648
|
+
points: List[float] = []
|
|
649
|
+
|
|
650
|
+
def _arc(cx: float, cy: float, r: float, start_deg: float) -> None:
|
|
651
|
+
if r <= 0:
|
|
652
|
+
points.extend((cx, cy))
|
|
653
|
+
return
|
|
654
|
+
for step in range(_RADIUS_ARC_STEPS + 1):
|
|
655
|
+
a = math.radians(start_deg - 90.0 * step / _RADIUS_ARC_STEPS)
|
|
656
|
+
points.extend((cx + r * math.cos(a), cy - r * math.sin(a)))
|
|
657
|
+
|
|
658
|
+
_arc(tl, tl, tl, 180.0)
|
|
659
|
+
_arc(w - tr, tr, tr, 90.0)
|
|
660
|
+
_arc(w - br, h - br, br, 0.0)
|
|
661
|
+
_arc(bl, h - bl, bl, -90.0)
|
|
662
|
+
return points
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _redraw_rounded_background(widget: Any) -> None:
|
|
666
|
+
"""Repaint the rounded background canvas if its inputs changed.
|
|
667
|
+
|
|
668
|
+
Sizes prefer the layout frame (kept current by ``_place``) over
|
|
669
|
+
``winfo_*`` so the polygon is right even before Tk maps the
|
|
670
|
+
window. Redraws are skipped when the (size, colors, radii,
|
|
671
|
+
underlay) signature matches the last paint.
|
|
672
|
+
"""
|
|
673
|
+
canvas = getattr(widget, "_pn_radius_canvas", None)
|
|
674
|
+
state = getattr(widget, "_pn_radius_state", None)
|
|
675
|
+
if canvas is None or state is None:
|
|
676
|
+
return
|
|
677
|
+
frame = getattr(widget, "_pn_frame", None)
|
|
678
|
+
if frame is not None and frame[2] > 0 and frame[3] > 0:
|
|
679
|
+
w, h = frame[2], frame[3]
|
|
680
|
+
else:
|
|
681
|
+
try:
|
|
682
|
+
w, h = float(canvas.winfo_width()), float(canvas.winfo_height())
|
|
683
|
+
except Exception:
|
|
684
|
+
return
|
|
685
|
+
if w <= 1 or h <= 1:
|
|
686
|
+
return
|
|
687
|
+
under = _underlay_color(widget)
|
|
688
|
+
signature = (w, h, state["radii"], state["fill"], state["outline"], state["outline_width"], under)
|
|
689
|
+
if state.get("drawn") == signature:
|
|
690
|
+
return
|
|
691
|
+
state["drawn"] = signature
|
|
692
|
+
try:
|
|
693
|
+
canvas.configure(background=under)
|
|
694
|
+
canvas.delete("all")
|
|
695
|
+
if state["fill"] or state["outline"]:
|
|
696
|
+
canvas.create_polygon(
|
|
697
|
+
_rounded_rect_points(w, h, state["radii"]),
|
|
698
|
+
fill=state["fill"] or "",
|
|
699
|
+
outline=state["outline"],
|
|
700
|
+
width=max(1.0, state["outline_width"]),
|
|
701
|
+
)
|
|
702
|
+
except Exception:
|
|
703
|
+
diagnostics.swallowed("desktop._redraw_rounded_background")
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _set_rounded_background(widget: Any, radii: Tuple[float, float, float, float], props: Dict[str, Any]) -> None:
|
|
707
|
+
"""Install (or refresh) the rounded background canvas on ``widget``."""
|
|
708
|
+
border_width, border_color = _resolve_border(props)
|
|
709
|
+
widget._pn_radius_state = {
|
|
710
|
+
"radii": radii,
|
|
711
|
+
"fill": _tk_color(props.get("background_color")),
|
|
712
|
+
"outline": border_color if border_width else "",
|
|
713
|
+
"outline_width": border_width,
|
|
714
|
+
}
|
|
715
|
+
canvas = getattr(widget, "_pn_radius_canvas", None)
|
|
716
|
+
if canvas is None:
|
|
717
|
+
try:
|
|
718
|
+
canvas = tk.Canvas(widget, highlightthickness=0, bd=0)
|
|
719
|
+
canvas.place(x=0, y=0, relwidth=1.0, relheight=1.0)
|
|
720
|
+
# Below any direct Tk children the handler owns (TabBar /
|
|
721
|
+
# SegmentedControl buttons); PythonNative children live on
|
|
722
|
+
# the stage and are lifted above the whole frame anyway.
|
|
723
|
+
# ``Canvas.lower`` is the canvas-item method, so reach for
|
|
724
|
+
# the widget-stacking ``Misc.lower`` explicitly.
|
|
725
|
+
tk.Misc.lower(canvas)
|
|
726
|
+
# Clicks over the frame now land on the covering canvas,
|
|
727
|
+
# so append the frame's bindtag: its gesture and press
|
|
728
|
+
# bindings keep firing, with matching coordinates (the
|
|
729
|
+
# canvas fills the frame exactly).
|
|
730
|
+
canvas.bindtags(tuple(canvas.bindtags()) + (str(widget),))
|
|
731
|
+
canvas.bind("<Configure>", lambda _e: _redraw_rounded_background(widget), add="+")
|
|
732
|
+
except Exception:
|
|
733
|
+
diagnostics.swallowed("desktop._set_rounded_background")
|
|
734
|
+
return
|
|
735
|
+
canvas._pn_parent = widget # wheel hit-testing walks through
|
|
736
|
+
widget._pn_radius_canvas = canvas
|
|
737
|
+
_set_pointer_muted(canvas, _pointer_muted(widget))
|
|
738
|
+
_redraw_rounded_background(widget)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _clear_rounded_background(widget: Any) -> None:
|
|
742
|
+
"""Drop the rounded background canvas when radii return to zero."""
|
|
743
|
+
canvas = getattr(widget, "_pn_radius_canvas", None)
|
|
744
|
+
if canvas is None:
|
|
745
|
+
return
|
|
746
|
+
widget._pn_radius_canvas = None
|
|
747
|
+
widget._pn_radius_state = None
|
|
748
|
+
try:
|
|
749
|
+
canvas.destroy()
|
|
750
|
+
except Exception:
|
|
751
|
+
diagnostics.swallowed("desktop._clear_rounded_background")
|
|
752
|
+
|
|
753
|
+
|
|
349
754
|
def _set_translate_from_transform(widget: Any, spec: Any) -> None:
|
|
350
755
|
"""Extract a translate offset from a ``transform`` prop for placement.
|
|
351
756
|
|
|
@@ -369,44 +774,40 @@ def _set_translate_from_transform(widget: Any, spec: Any) -> None:
|
|
|
369
774
|
|
|
370
775
|
def _apply_common(widget: Any, props: Dict[str, Any]) -> None:
|
|
371
776
|
"""Apply visual props shared across most handlers (bg, border, transform)."""
|
|
372
|
-
if
|
|
777
|
+
radii = _corner_radii(props) if isinstance(widget, tk.Frame) else (0.0, 0.0, 0.0, 0.0)
|
|
778
|
+
rounded = any(r > 0 for r in radii)
|
|
779
|
+
if "background_color" in props and not rounded:
|
|
373
780
|
color = _tk_color(props["background_color"])
|
|
374
781
|
if color is not None:
|
|
375
782
|
try:
|
|
376
783
|
widget.configure(background=color)
|
|
377
784
|
except Exception:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
"border_right_width",
|
|
383
|
-
"border_bottom_width",
|
|
384
|
-
)
|
|
385
|
-
if any(k in props for k in ("border_width", "border_color", *side_width_keys)):
|
|
785
|
+
diagnostics.swallowed("desktop._apply_common")
|
|
786
|
+
if rounded:
|
|
787
|
+
# Background and border move onto the rounded canvas; the
|
|
788
|
+
# square highlight border would poke out of the corners.
|
|
386
789
|
try:
|
|
387
|
-
|
|
388
|
-
color = _tk_color(props.get("border_color")) or "#3c3c43"
|
|
389
|
-
# Tk highlight borders are uniform, so per-side borders are
|
|
390
|
-
# approximated with the widest side's width and its color.
|
|
391
|
-
side_widths = [props.get(k) for k in side_width_keys]
|
|
392
|
-
if any(w is not None for w in side_widths):
|
|
393
|
-
width = max(float(w) for w in side_widths if w is not None)
|
|
394
|
-
for side, w in zip(("left", "top", "right", "bottom"), side_widths):
|
|
395
|
-
if w is not None and float(w) > 0:
|
|
396
|
-
side_color = _tk_color(props.get(f"border_{side}_color"))
|
|
397
|
-
if side_color is not None:
|
|
398
|
-
color = side_color
|
|
399
|
-
break
|
|
400
|
-
if width:
|
|
401
|
-
widget.configure(
|
|
402
|
-
highlightthickness=int(round(_finite(width))),
|
|
403
|
-
highlightbackground=color,
|
|
404
|
-
highlightcolor=color,
|
|
405
|
-
)
|
|
406
|
-
else:
|
|
407
|
-
widget.configure(highlightthickness=0)
|
|
790
|
+
widget.configure(highlightthickness=0)
|
|
408
791
|
except Exception:
|
|
409
|
-
|
|
792
|
+
diagnostics.swallowed("desktop._apply_common")
|
|
793
|
+
_set_rounded_background(widget, radii, props)
|
|
794
|
+
else:
|
|
795
|
+
_clear_rounded_background(widget)
|
|
796
|
+
if any(k in props for k in ("border_width", "border_color", *_SIDE_BORDER_WIDTH_KEYS)):
|
|
797
|
+
try:
|
|
798
|
+
width, color = _resolve_border(props)
|
|
799
|
+
if width:
|
|
800
|
+
widget.configure(
|
|
801
|
+
highlightthickness=int(round(width)),
|
|
802
|
+
highlightbackground=color,
|
|
803
|
+
highlightcolor=color,
|
|
804
|
+
)
|
|
805
|
+
else:
|
|
806
|
+
widget.configure(highlightthickness=0)
|
|
807
|
+
except Exception:
|
|
808
|
+
diagnostics.swallowed("desktop._apply_common")
|
|
809
|
+
if "hit_slop" in props:
|
|
810
|
+
widget._pn_hit_slop = _parse_hit_slop(props.get("hit_slop"))
|
|
410
811
|
if "test_id" in props and props["test_id"] is not None:
|
|
411
812
|
# Stamped for introspection so preview-level tooling and tests
|
|
412
813
|
# can locate widgets the same way Maestro does on device.
|
|
@@ -414,6 +815,13 @@ def _apply_common(widget: Any, props: Dict[str, Any]) -> None:
|
|
|
414
815
|
if "transform" in props:
|
|
415
816
|
_set_translate_from_transform(widget, props["transform"])
|
|
416
817
|
_place(widget)
|
|
818
|
+
if "z_index" in props:
|
|
819
|
+
parent = getattr(widget, "_pn_parent", None)
|
|
820
|
+
if parent is not None:
|
|
821
|
+
parent._pn_has_z = True
|
|
822
|
+
_restack(parent)
|
|
823
|
+
if "pointer_events" in props:
|
|
824
|
+
_refresh_pointer_events(widget)
|
|
417
825
|
|
|
418
826
|
|
|
419
827
|
# ======================================================================
|
|
@@ -460,7 +868,7 @@ def _wire_gestures(widget: Any, specs: Any) -> None:
|
|
|
460
868
|
try:
|
|
461
869
|
widget.after(delay_ms, _poll)
|
|
462
870
|
except Exception:
|
|
463
|
-
|
|
871
|
+
diagnostics.swallowed("desktop._wire_gestures._schedule_poll")
|
|
464
872
|
|
|
465
873
|
def _on_down(event: Any) -> None:
|
|
466
874
|
current = getattr(widget, "_pn_arbiter", None)
|
|
@@ -483,7 +891,7 @@ def _wire_gestures(widget: Any, specs: Any) -> None:
|
|
|
483
891
|
widget.bind("<B1-Motion>", _on_move, add="+")
|
|
484
892
|
widget.bind("<ButtonRelease-1>", _on_up, add="+")
|
|
485
893
|
except Exception:
|
|
486
|
-
|
|
894
|
+
diagnostics.swallowed("desktop._wire_gestures")
|
|
487
895
|
|
|
488
896
|
|
|
489
897
|
# ======================================================================
|
|
@@ -533,11 +941,19 @@ def _install_wheel_bindings(container: Any) -> None:
|
|
|
533
941
|
top.bind_all("<Button-5>", _on_wheel, add="+")
|
|
534
942
|
_WHEEL_BOUND = True
|
|
535
943
|
except Exception:
|
|
536
|
-
|
|
944
|
+
diagnostics.swallowed("desktop._install_wheel_bindings")
|
|
537
945
|
|
|
538
946
|
|
|
539
947
|
def _content_extent(widget: Any) -> Tuple[float, float]:
|
|
540
|
-
"""Max (right, bottom) edge over the scroll container's children.
|
|
948
|
+
"""Max (right, bottom) edge over the scroll container's children.
|
|
949
|
+
|
|
950
|
+
Containers that window their children (``VirtualList``) publish
|
|
951
|
+
the full logical extent via ``_pn_content_extent`` so clamping
|
|
952
|
+
covers rows that aren't currently mounted.
|
|
953
|
+
"""
|
|
954
|
+
override = getattr(widget, "_pn_content_extent", None)
|
|
955
|
+
if override is not None:
|
|
956
|
+
return override
|
|
541
957
|
max_x = 0.0
|
|
542
958
|
max_y = 0.0
|
|
543
959
|
for child in getattr(widget, "_pn_children", []) or []:
|
|
@@ -564,7 +980,12 @@ def _scroll_to(widget: Any, x: float, y: float, fire_event: bool = True) -> None
|
|
|
564
980
|
widget._pn_scroll_offset = new_offset
|
|
565
981
|
for child in getattr(widget, "_pn_children", []) or []:
|
|
566
982
|
_place(child)
|
|
567
|
-
|
|
983
|
+
hook = getattr(widget, "_pn_scroll_hook", None)
|
|
984
|
+
if hook is not None:
|
|
985
|
+
# ``VirtualList`` re-windows its rows and reports the richer
|
|
986
|
+
# native-list scroll payload instead of the default event.
|
|
987
|
+
hook(new_offset, fire_event)
|
|
988
|
+
elif fire_event:
|
|
568
989
|
_fire(widget, "on_scroll", {"x": new_offset[0], "y": new_offset[1]})
|
|
569
990
|
|
|
570
991
|
|
|
@@ -621,13 +1042,22 @@ class DesktopViewHandler(ViewHandler):
|
|
|
621
1042
|
child._pn_parent = parent
|
|
622
1043
|
_register_child(parent, child, index)
|
|
623
1044
|
_place(child)
|
|
1045
|
+
# ``z_index`` applies before insertion (create-time props run
|
|
1046
|
+
# first), so stacking and pointer muting settle here, once the
|
|
1047
|
+
# parent chain is known.
|
|
1048
|
+
merged = getattr(child, "_pn_props", None) or {}
|
|
1049
|
+
if merged.get("z_index") is not None:
|
|
1050
|
+
parent._pn_has_z = True
|
|
1051
|
+
if getattr(parent, "_pn_has_z", False):
|
|
1052
|
+
_restack(parent)
|
|
1053
|
+
_refresh_pointer_events(child)
|
|
624
1054
|
|
|
625
1055
|
def remove_child(self, parent: Any, child: Any) -> None:
|
|
626
1056
|
_unregister_child(parent, child)
|
|
627
1057
|
try:
|
|
628
1058
|
child.place_forget()
|
|
629
1059
|
except Exception:
|
|
630
|
-
|
|
1060
|
+
diagnostics.swallowed("desktop.DesktopViewHandler.remove_child")
|
|
631
1061
|
child._pn_parent = None
|
|
632
1062
|
|
|
633
1063
|
def destroy(self, native_view: Any) -> None:
|
|
@@ -638,7 +1068,7 @@ class DesktopViewHandler(ViewHandler):
|
|
|
638
1068
|
try:
|
|
639
1069
|
native_view.destroy()
|
|
640
1070
|
except Exception:
|
|
641
|
-
|
|
1071
|
+
diagnostics.swallowed("desktop.DesktopViewHandler.destroy")
|
|
642
1072
|
|
|
643
1073
|
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
644
1074
|
if native_view is None:
|
|
@@ -652,9 +1082,14 @@ class DesktopViewHandler(ViewHandler):
|
|
|
652
1082
|
def set_animated_property(self, native_view: Any, prop_name: str, value: Any) -> None:
|
|
653
1083
|
"""Apply one frame of a Python-driven animation.
|
|
654
1084
|
|
|
655
|
-
Translation maps onto placement;
|
|
656
|
-
``
|
|
657
|
-
|
|
1085
|
+
Translation maps onto placement; ``background_color`` and
|
|
1086
|
+
``color`` map onto ``configure`` and accept every color form
|
|
1087
|
+
``_tk_color`` understands, including the ``"#AARRGGBB"``
|
|
1088
|
+
strings that color interpolations emit (alpha is dropped).
|
|
1089
|
+
Opacity, scale, and rotation (numeric degrees) have no cheap
|
|
1090
|
+
Tk analogue, and other animated style keys have no desktop
|
|
1091
|
+
mapping; those frames are silently skipped (a documented
|
|
1092
|
+
preview limitation).
|
|
658
1093
|
"""
|
|
659
1094
|
if native_view is None:
|
|
660
1095
|
return
|
|
@@ -669,10 +1104,24 @@ class DesktopViewHandler(ViewHandler):
|
|
|
669
1104
|
elif prop_name == "background_color":
|
|
670
1105
|
color = _tk_color(value)
|
|
671
1106
|
if color is not None:
|
|
1107
|
+
state = getattr(native_view, "_pn_radius_state", None)
|
|
1108
|
+
if state is not None:
|
|
1109
|
+
# A rounded view paints its background on the
|
|
1110
|
+
# radius canvas, not the frame itself.
|
|
1111
|
+
state["fill"] = color
|
|
1112
|
+
_redraw_rounded_background(native_view)
|
|
1113
|
+
return
|
|
672
1114
|
try:
|
|
673
1115
|
native_view.configure(background=color)
|
|
674
1116
|
except Exception:
|
|
675
|
-
|
|
1117
|
+
diagnostics.swallowed("desktop.DesktopViewHandler.set_animated_property")
|
|
1118
|
+
elif prop_name == "color":
|
|
1119
|
+
color = _tk_color(value)
|
|
1120
|
+
if color is not None:
|
|
1121
|
+
try:
|
|
1122
|
+
native_view.configure(foreground=color)
|
|
1123
|
+
except Exception:
|
|
1124
|
+
diagnostics.swallowed("desktop.DesktopViewHandler.set_animated_property")
|
|
676
1125
|
|
|
677
1126
|
|
|
678
1127
|
# ======================================================================
|
|
@@ -742,6 +1191,257 @@ class ScrollViewHandler(FlexContainerHandler):
|
|
|
742
1191
|
_scroll_to(native_view, sx, sy, fire_event=False)
|
|
743
1192
|
|
|
744
1193
|
|
|
1194
|
+
# ======================================================================
|
|
1195
|
+
# VirtualList (natively virtualized list preview)
|
|
1196
|
+
# ======================================================================
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
class VirtualListHandler(ScrollViewHandler):
|
|
1200
|
+
"""Preview twin of the native ``VirtualList`` handlers.
|
|
1201
|
+
|
|
1202
|
+
Android backs ``VirtualList`` with a ``RecyclerView`` and iOS with
|
|
1203
|
+
a ``UITableView``; the desktop preview reuses the ScrollView
|
|
1204
|
+
machinery and does its own row windowing so trees that target the
|
|
1205
|
+
native-list contract behave the same way here. Rows within one
|
|
1206
|
+
viewport of the scroll window host a live subtree driven by a
|
|
1207
|
+
nested reconciler (see ``pythonnative.virtual_rows``); rows that
|
|
1208
|
+
leave the window are unmounted.
|
|
1209
|
+
|
|
1210
|
+
Expects props:
|
|
1211
|
+
|
|
1212
|
+
- ``count``: total number of rows.
|
|
1213
|
+
- ``row_height``: uniform row extent in points, or
|
|
1214
|
+
- ``row_heights``: per-row extents.
|
|
1215
|
+
- ``render_row``: ``render_row(index) -> Element`` producing one
|
|
1216
|
+
row's subtree. Called lazily as rows enter the window.
|
|
1217
|
+
- ``shows_scroll_indicator``: accepted and ignored (the preview
|
|
1218
|
+
draws no scroll bar).
|
|
1219
|
+
|
|
1220
|
+
Events (dispatched by tag): ``on_scroll`` with
|
|
1221
|
+
``{"x", "y", "extent", "range"}`` in points, matching the device
|
|
1222
|
+
handlers. ``on_row_press`` is not synthesized: preview rows are
|
|
1223
|
+
live widgets, so presses land on the row's own Pressable / Button
|
|
1224
|
+
children.
|
|
1225
|
+
|
|
1226
|
+
Commands: ``scroll_to_offset`` / ``scroll_to_index`` /
|
|
1227
|
+
``scroll_to_end`` / ``get_scroll_offset``. The ``animated`` flag
|
|
1228
|
+
is accepted and ignored (the preview jumps, like ScrollView).
|
|
1229
|
+
"""
|
|
1230
|
+
|
|
1231
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1232
|
+
from ..virtual_rows import RowHostPool
|
|
1233
|
+
|
|
1234
|
+
frame = super().build(props)
|
|
1235
|
+
frame._pn_vl = {
|
|
1236
|
+
"count": 0,
|
|
1237
|
+
"row_height": 44.0,
|
|
1238
|
+
"row_heights": None,
|
|
1239
|
+
"render_row": None,
|
|
1240
|
+
"starts": [0.0],
|
|
1241
|
+
"pool": RowHostPool(),
|
|
1242
|
+
"cells": {},
|
|
1243
|
+
"cell_width": 0.0,
|
|
1244
|
+
}
|
|
1245
|
+
frame._pn_content_extent = (0.0, 0.0)
|
|
1246
|
+
|
|
1247
|
+
def _hook(offset: Tuple[float, float], fire_event: bool) -> None:
|
|
1248
|
+
self._rewindow(frame)
|
|
1249
|
+
if fire_event:
|
|
1250
|
+
viewport = getattr(frame, "_pn_frame", None)
|
|
1251
|
+
_fire(
|
|
1252
|
+
frame,
|
|
1253
|
+
"on_scroll",
|
|
1254
|
+
{
|
|
1255
|
+
"x": 0.0,
|
|
1256
|
+
"y": offset[1],
|
|
1257
|
+
"extent": viewport[3] if viewport else 0.0,
|
|
1258
|
+
"range": frame._pn_vl["starts"][-1],
|
|
1259
|
+
},
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
frame._pn_scroll_hook = _hook
|
|
1263
|
+
return frame
|
|
1264
|
+
|
|
1265
|
+
def apply(self, frame: Any, props: Dict[str, Any]) -> None:
|
|
1266
|
+
super().apply(frame, props)
|
|
1267
|
+
layout_changed, content_changed = self._read_data_props(frame, props)
|
|
1268
|
+
if layout_changed:
|
|
1269
|
+
# Geometry changed under the mounted window; rebuild it.
|
|
1270
|
+
self._release_rows(frame)
|
|
1271
|
+
self._rewindow(frame)
|
|
1272
|
+
elif content_changed:
|
|
1273
|
+
# Only ``render_row`` changed (a fresh closure every
|
|
1274
|
+
# render); reconcile the live rows in place, mirroring
|
|
1275
|
+
# the device handlers' reload-on-render behavior.
|
|
1276
|
+
self._rebind_rows(frame)
|
|
1277
|
+
|
|
1278
|
+
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1279
|
+
super().set_frame(native_view, x, y, width, height)
|
|
1280
|
+
self._rewindow(native_view)
|
|
1281
|
+
|
|
1282
|
+
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
1283
|
+
# Fill the available space, like a ScrollView clamped to its
|
|
1284
|
+
# parent; collapse to 0 on unbounded axes (nested lists don't
|
|
1285
|
+
# scroll, matching the device handlers).
|
|
1286
|
+
w = max_width if math.isfinite(max_width) else 0.0
|
|
1287
|
+
h = max_height if math.isfinite(max_height) else 0.0
|
|
1288
|
+
return (max(0.0, w), max(0.0, h))
|
|
1289
|
+
|
|
1290
|
+
def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
1291
|
+
info = getattr(native_view, "_pn_vl", None)
|
|
1292
|
+
if info is None:
|
|
1293
|
+
return None
|
|
1294
|
+
if name == "scroll_to_offset":
|
|
1295
|
+
_scroll_to(native_view, 0.0, _finite(args.get("y", 0.0)))
|
|
1296
|
+
return True
|
|
1297
|
+
if name == "scroll_to_index":
|
|
1298
|
+
count = info["count"]
|
|
1299
|
+
index = max(0, min(int(_finite(args.get("index", 0))), max(0, count - 1)))
|
|
1300
|
+
_scroll_to(native_view, 0.0, info["starts"][index] if count else 0.0)
|
|
1301
|
+
return True
|
|
1302
|
+
if name == "scroll_to_end":
|
|
1303
|
+
_scroll_to(native_view, 0.0, info["starts"][-1])
|
|
1304
|
+
return True
|
|
1305
|
+
if name == "get_scroll_offset":
|
|
1306
|
+
sx, sy = getattr(native_view, "_pn_scroll_offset", (0.0, 0.0))
|
|
1307
|
+
return {"x": sx, "y": sy}
|
|
1308
|
+
return None
|
|
1309
|
+
|
|
1310
|
+
def destroy(self, native_view: Any) -> None:
|
|
1311
|
+
info = getattr(native_view, "_pn_vl", None)
|
|
1312
|
+
if info is not None:
|
|
1313
|
+
self._release_rows(native_view)
|
|
1314
|
+
try:
|
|
1315
|
+
info["pool"].release_all()
|
|
1316
|
+
except Exception:
|
|
1317
|
+
diagnostics.swallowed("desktop.VirtualListHandler.destroy")
|
|
1318
|
+
super().destroy(native_view)
|
|
1319
|
+
|
|
1320
|
+
# -- data + windowing ----------------------------------------------
|
|
1321
|
+
|
|
1322
|
+
def _read_data_props(self, frame: Any, props: Dict[str, Any]) -> Tuple[bool, bool]:
|
|
1323
|
+
"""Fold changed data props into ``_pn_vl``.
|
|
1324
|
+
|
|
1325
|
+
Returns ``(layout_changed, content_changed)``: the first is
|
|
1326
|
+
true when row geometry changed (count or extents) and the
|
|
1327
|
+
window must be rebuilt; the second when ``render_row`` changed
|
|
1328
|
+
and live rows only need a rebind.
|
|
1329
|
+
"""
|
|
1330
|
+
info = frame._pn_vl
|
|
1331
|
+
layout_changed = False
|
|
1332
|
+
if "count" in props:
|
|
1333
|
+
info["count"] = int(props.get("count") or 0)
|
|
1334
|
+
layout_changed = True
|
|
1335
|
+
if "row_height" in props and props.get("row_height") is not None:
|
|
1336
|
+
info["row_height"] = max(0.0, _finite(props["row_height"], 44.0))
|
|
1337
|
+
layout_changed = True
|
|
1338
|
+
if "row_heights" in props:
|
|
1339
|
+
heights = props.get("row_heights")
|
|
1340
|
+
info["row_heights"] = [max(0.0, _finite(h)) for h in heights] if heights else None
|
|
1341
|
+
layout_changed = True
|
|
1342
|
+
content_changed = False
|
|
1343
|
+
if "render_row" in props:
|
|
1344
|
+
info["render_row"] = props.get("render_row")
|
|
1345
|
+
content_changed = True
|
|
1346
|
+
if layout_changed:
|
|
1347
|
+
n = info["count"]
|
|
1348
|
+
heights = info["row_heights"]
|
|
1349
|
+
starts = [0.0] * (n + 1)
|
|
1350
|
+
acc = 0.0
|
|
1351
|
+
for i in range(n):
|
|
1352
|
+
starts[i] = acc
|
|
1353
|
+
extent = heights[i] if heights is not None and i < len(heights) else info["row_height"]
|
|
1354
|
+
acc += max(0.0, extent)
|
|
1355
|
+
starts[n] = acc
|
|
1356
|
+
info["starts"] = starts
|
|
1357
|
+
# Publish the full content extent so ``_scroll_to`` clamps
|
|
1358
|
+
# against every row, not just the mounted window.
|
|
1359
|
+
frame._pn_content_extent = (0.0, acc)
|
|
1360
|
+
return (layout_changed, content_changed)
|
|
1361
|
+
|
|
1362
|
+
def _rewindow(self, frame: Any) -> None:
|
|
1363
|
+
"""Mount rows within one viewport of the window, unmount the rest."""
|
|
1364
|
+
info = getattr(frame, "_pn_vl", None)
|
|
1365
|
+
if info is None or info["render_row"] is None:
|
|
1366
|
+
return
|
|
1367
|
+
viewport = getattr(frame, "_pn_frame", None)
|
|
1368
|
+
if viewport is None:
|
|
1369
|
+
return
|
|
1370
|
+
n = info["count"]
|
|
1371
|
+
width = max(0.0, viewport[2])
|
|
1372
|
+
height = max(0.0, viewport[3])
|
|
1373
|
+
if n <= 0 or width <= 0 or height <= 0:
|
|
1374
|
+
self._release_rows(frame)
|
|
1375
|
+
return
|
|
1376
|
+
if width != info["cell_width"]:
|
|
1377
|
+
# Mounted rows were laid out against a different width.
|
|
1378
|
+
self._release_rows(frame)
|
|
1379
|
+
info["cell_width"] = width
|
|
1380
|
+
starts = info["starts"]
|
|
1381
|
+
offset = getattr(frame, "_pn_scroll_offset", (0.0, 0.0))[1]
|
|
1382
|
+
lo = max(0.0, offset - height)
|
|
1383
|
+
hi = offset + 2.0 * height
|
|
1384
|
+
first = max(0, bisect.bisect_right(starts, lo, 0, n) - 1)
|
|
1385
|
+
last = min(n - 1, bisect.bisect_left(starts, hi, 0, n))
|
|
1386
|
+
cells: Dict[int, Any] = info["cells"]
|
|
1387
|
+
for index in [i for i in cells if i < first or i > last]:
|
|
1388
|
+
self._release_row(frame, index)
|
|
1389
|
+
for index in range(first, last + 1):
|
|
1390
|
+
if index in cells:
|
|
1391
|
+
continue
|
|
1392
|
+
cell = tk.Frame(_master(), highlightthickness=0, bd=0)
|
|
1393
|
+
cell._pn_parent = frame
|
|
1394
|
+
cells[index] = cell
|
|
1395
|
+
_register_child(frame, cell, len(getattr(frame, "_pn_children", None) or []))
|
|
1396
|
+
cell._pn_frame = (0.0, starts[index], width, starts[index + 1] - starts[index])
|
|
1397
|
+
_place(cell)
|
|
1398
|
+
self._attach_row(frame, cell, index)
|
|
1399
|
+
|
|
1400
|
+
def _attach_row(self, frame: Any, cell: Any, index: int) -> None:
|
|
1401
|
+
"""Mount (or rebind) row ``index`` into ``cell`` and place its root."""
|
|
1402
|
+
info = frame._pn_vl
|
|
1403
|
+
render_row = info["render_row"]
|
|
1404
|
+
cell_frame = getattr(cell, "_pn_frame", (0.0, 0.0, 0.0, 0.0))
|
|
1405
|
+
try:
|
|
1406
|
+
root = info["pool"].bind(index, lambda: render_row(index), cell_frame[2], cell_frame[3])
|
|
1407
|
+
except Exception:
|
|
1408
|
+
diagnostics.swallowed("desktop.VirtualListHandler._attach_row")
|
|
1409
|
+
return
|
|
1410
|
+
if root is None:
|
|
1411
|
+
return
|
|
1412
|
+
# A rebind can replace the subtree's root, so reset the cell's
|
|
1413
|
+
# child list instead of accumulating stale widgets.
|
|
1414
|
+
root._pn_parent = cell
|
|
1415
|
+
cell._pn_children = [root]
|
|
1416
|
+
_place(root)
|
|
1417
|
+
|
|
1418
|
+
def _rebind_rows(self, frame: Any) -> None:
|
|
1419
|
+
info = frame._pn_vl
|
|
1420
|
+
for index, cell in list(info["cells"].items()):
|
|
1421
|
+
self._attach_row(frame, cell, index)
|
|
1422
|
+
|
|
1423
|
+
def _release_row(self, frame: Any, index: int) -> None:
|
|
1424
|
+
info = frame._pn_vl
|
|
1425
|
+
cell = info["cells"].pop(index, None)
|
|
1426
|
+
try:
|
|
1427
|
+
info["pool"].release(index)
|
|
1428
|
+
except Exception:
|
|
1429
|
+
diagnostics.swallowed("desktop.VirtualListHandler._release_row")
|
|
1430
|
+
if cell is not None:
|
|
1431
|
+
_unregister_child(frame, cell)
|
|
1432
|
+
try:
|
|
1433
|
+
cell.destroy()
|
|
1434
|
+
except Exception:
|
|
1435
|
+
diagnostics.swallowed("desktop.VirtualListHandler._release_row")
|
|
1436
|
+
|
|
1437
|
+
def _release_rows(self, frame: Any) -> None:
|
|
1438
|
+
info = getattr(frame, "_pn_vl", None)
|
|
1439
|
+
if info is None:
|
|
1440
|
+
return
|
|
1441
|
+
for index in list(info["cells"]):
|
|
1442
|
+
self._release_row(frame, index)
|
|
1443
|
+
|
|
1444
|
+
|
|
745
1445
|
# ======================================================================
|
|
746
1446
|
# Text
|
|
747
1447
|
# ======================================================================
|
|
@@ -775,7 +1475,7 @@ class TextHandler(DesktopViewHandler):
|
|
|
775
1475
|
try:
|
|
776
1476
|
label.configure(**opts)
|
|
777
1477
|
except Exception:
|
|
778
|
-
|
|
1478
|
+
diagnostics.swallowed("desktop.TextHandler.apply")
|
|
779
1479
|
_apply_common(label, merged)
|
|
780
1480
|
|
|
781
1481
|
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
@@ -784,7 +1484,7 @@ class TextHandler(DesktopViewHandler):
|
|
|
784
1484
|
try:
|
|
785
1485
|
native_view.configure(wraplength=max(1, int(_finite(width))))
|
|
786
1486
|
except Exception:
|
|
787
|
-
|
|
1487
|
+
diagnostics.swallowed("desktop.TextHandler.set_frame")
|
|
788
1488
|
super().set_frame(native_view, x, y, width, height)
|
|
789
1489
|
|
|
790
1490
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
@@ -825,7 +1525,7 @@ class ButtonHandler(DesktopViewHandler):
|
|
|
825
1525
|
try:
|
|
826
1526
|
button.configure(**opts)
|
|
827
1527
|
except Exception:
|
|
828
|
-
|
|
1528
|
+
diagnostics.swallowed("desktop.ButtonHandler.apply")
|
|
829
1529
|
_apply_common(button, merged)
|
|
830
1530
|
|
|
831
1531
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
@@ -873,7 +1573,7 @@ class TextInputHandler(DesktopViewHandler):
|
|
|
873
1573
|
widget.delete(0, "end")
|
|
874
1574
|
widget.insert(0, value)
|
|
875
1575
|
except Exception:
|
|
876
|
-
|
|
1576
|
+
diagnostics.swallowed("desktop.TextInputHandler._set_text")
|
|
877
1577
|
finally:
|
|
878
1578
|
widget._pn_suppress = False
|
|
879
1579
|
|
|
@@ -900,7 +1600,7 @@ class TextInputHandler(DesktopViewHandler):
|
|
|
900
1600
|
if not getattr(widget, "_pn_multiline", False):
|
|
901
1601
|
widget.bind("<Return>", _on_return)
|
|
902
1602
|
except Exception:
|
|
903
|
-
|
|
1603
|
+
diagnostics.swallowed("desktop.TextInputHandler._bind")
|
|
904
1604
|
|
|
905
1605
|
def apply(self, widget: Any, props: Dict[str, Any]) -> None:
|
|
906
1606
|
merged = getattr(widget, "_pn_props", props)
|
|
@@ -918,7 +1618,7 @@ class TextInputHandler(DesktopViewHandler):
|
|
|
918
1618
|
try:
|
|
919
1619
|
widget.configure(**opts)
|
|
920
1620
|
except Exception:
|
|
921
|
-
|
|
1621
|
+
diagnostics.swallowed("desktop.TextInputHandler.apply")
|
|
922
1622
|
if "value" in props:
|
|
923
1623
|
incoming = "" if props["value"] is None else str(props["value"])
|
|
924
1624
|
if self._current_text(widget) != incoming:
|
|
@@ -927,20 +1627,20 @@ class TextInputHandler(DesktopViewHandler):
|
|
|
927
1627
|
try:
|
|
928
1628
|
widget.configure(highlightbackground="#c7c7cc", highlightcolor="#007aff")
|
|
929
1629
|
except Exception:
|
|
930
|
-
|
|
1630
|
+
diagnostics.swallowed("desktop.TextInputHandler.apply")
|
|
931
1631
|
|
|
932
1632
|
def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
933
1633
|
if name == "focus":
|
|
934
1634
|
try:
|
|
935
1635
|
native_view.focus_set()
|
|
936
1636
|
except Exception:
|
|
937
|
-
|
|
1637
|
+
diagnostics.swallowed("desktop.TextInputHandler.command")
|
|
938
1638
|
return True
|
|
939
1639
|
if name == "blur":
|
|
940
1640
|
try:
|
|
941
1641
|
native_view.winfo_toplevel().focus_set()
|
|
942
1642
|
except Exception:
|
|
943
|
-
|
|
1643
|
+
diagnostics.swallowed("desktop.TextInputHandler.command")
|
|
944
1644
|
return True
|
|
945
1645
|
return None
|
|
946
1646
|
|
|
@@ -982,7 +1682,7 @@ class ImageHandler(DesktopViewHandler):
|
|
|
982
1682
|
try:
|
|
983
1683
|
label.configure(background=color)
|
|
984
1684
|
except Exception:
|
|
985
|
-
|
|
1685
|
+
diagnostics.swallowed("desktop.ImageHandler.apply")
|
|
986
1686
|
if "source" in props:
|
|
987
1687
|
source = props.get("source")
|
|
988
1688
|
label._pn_pending_source = source
|
|
@@ -1021,14 +1721,14 @@ class ImageHandler(DesktopViewHandler):
|
|
|
1021
1721
|
if path:
|
|
1022
1722
|
_fire(label, "on_error", "decode failed")
|
|
1023
1723
|
except Exception:
|
|
1024
|
-
|
|
1724
|
+
diagnostics.swallowed("desktop.ImageHandler._show_file")
|
|
1025
1725
|
|
|
1026
1726
|
def _show_fallback(self, label: Any, source: Any) -> None:
|
|
1027
1727
|
name = str(source).rsplit("/", 1)[-1] if source else "image"
|
|
1028
1728
|
try:
|
|
1029
1729
|
label.configure(image="", text=f"\U0001f5bc\n{name}", compound="center")
|
|
1030
1730
|
except Exception:
|
|
1031
|
-
|
|
1731
|
+
diagnostics.swallowed("desktop.ImageHandler._show_fallback")
|
|
1032
1732
|
|
|
1033
1733
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
1034
1734
|
photo = getattr(native_view, "_pn_photo", None)
|
|
@@ -1036,7 +1736,7 @@ class ImageHandler(DesktopViewHandler):
|
|
|
1036
1736
|
try:
|
|
1037
1737
|
return (float(photo.width()), float(photo.height()))
|
|
1038
1738
|
except Exception:
|
|
1039
|
-
|
|
1739
|
+
diagnostics.swallowed("desktop.ImageHandler.measure_intrinsic")
|
|
1040
1740
|
return (64.0, 64.0)
|
|
1041
1741
|
|
|
1042
1742
|
|
|
@@ -1059,7 +1759,7 @@ class SwitchHandler(DesktopViewHandler):
|
|
|
1059
1759
|
try:
|
|
1060
1760
|
check._pn_var.set(1 if props.get("value") else 0)
|
|
1061
1761
|
except Exception:
|
|
1062
|
-
|
|
1762
|
+
diagnostics.swallowed("desktop.SwitchHandler.apply")
|
|
1063
1763
|
_apply_common(check, merged)
|
|
1064
1764
|
|
|
1065
1765
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
@@ -1088,12 +1788,12 @@ class CheckboxHandler(DesktopViewHandler):
|
|
|
1088
1788
|
try:
|
|
1089
1789
|
check.configure(**opts)
|
|
1090
1790
|
except Exception:
|
|
1091
|
-
|
|
1791
|
+
diagnostics.swallowed("desktop.CheckboxHandler.apply")
|
|
1092
1792
|
if "value" in props:
|
|
1093
1793
|
try:
|
|
1094
1794
|
check._pn_var.set(1 if props.get("value") else 0)
|
|
1095
1795
|
except Exception:
|
|
1096
|
-
|
|
1796
|
+
diagnostics.swallowed("desktop.CheckboxHandler.apply")
|
|
1097
1797
|
_apply_common(check, merged)
|
|
1098
1798
|
|
|
1099
1799
|
|
|
@@ -1118,7 +1818,7 @@ class SliderHandler(DesktopViewHandler):
|
|
|
1118
1818
|
try:
|
|
1119
1819
|
_fire(scale, "on_change", float(scale.get()))
|
|
1120
1820
|
except Exception:
|
|
1121
|
-
|
|
1821
|
+
diagnostics.swallowed("desktop.SliderHandler.build._command")
|
|
1122
1822
|
|
|
1123
1823
|
scale.configure(command=_command)
|
|
1124
1824
|
return scale
|
|
@@ -1134,13 +1834,13 @@ class SliderHandler(DesktopViewHandler):
|
|
|
1134
1834
|
try:
|
|
1135
1835
|
scale.configure(**opts)
|
|
1136
1836
|
except Exception:
|
|
1137
|
-
|
|
1837
|
+
diagnostics.swallowed("desktop.SliderHandler.apply")
|
|
1138
1838
|
if "value" in merged:
|
|
1139
1839
|
scale._pn_suppress = True
|
|
1140
1840
|
try:
|
|
1141
1841
|
scale.set(_finite(merged.get("value")))
|
|
1142
1842
|
except Exception:
|
|
1143
|
-
|
|
1843
|
+
diagnostics.swallowed("desktop.SliderHandler.apply")
|
|
1144
1844
|
finally:
|
|
1145
1845
|
scale._pn_suppress = False
|
|
1146
1846
|
_apply_common(scale, merged)
|
|
@@ -1161,12 +1861,12 @@ class ProgressBarHandler(DesktopViewHandler):
|
|
|
1161
1861
|
bar.configure(mode="indeterminate")
|
|
1162
1862
|
bar.start(60)
|
|
1163
1863
|
except Exception:
|
|
1164
|
-
|
|
1864
|
+
diagnostics.swallowed("desktop.ProgressBarHandler.apply")
|
|
1165
1865
|
else:
|
|
1166
1866
|
try:
|
|
1167
1867
|
bar.configure(mode="determinate", value=max(0.0, min(1.0, _finite(merged.get("value", 0.0)))))
|
|
1168
1868
|
except Exception:
|
|
1169
|
-
|
|
1869
|
+
diagnostics.swallowed("desktop.ProgressBarHandler.apply")
|
|
1170
1870
|
|
|
1171
1871
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
1172
1872
|
width = max_width if math.isfinite(max_width) else 200.0
|
|
@@ -1185,7 +1885,7 @@ class ActivityIndicatorHandler(DesktopViewHandler):
|
|
|
1185
1885
|
else:
|
|
1186
1886
|
bar.stop()
|
|
1187
1887
|
except Exception:
|
|
1188
|
-
|
|
1888
|
+
diagnostics.swallowed("desktop.ActivityIndicatorHandler.apply")
|
|
1189
1889
|
|
|
1190
1890
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
1191
1891
|
merged = getattr(native_view, "_pn_props", {}) or {}
|
|
@@ -1228,7 +1928,7 @@ class WebViewHandler(DesktopViewHandler):
|
|
|
1228
1928
|
try:
|
|
1229
1929
|
label.configure(text=f"\U0001f310 WebView\n{target}")
|
|
1230
1930
|
except Exception:
|
|
1231
|
-
|
|
1931
|
+
diagnostics.swallowed("desktop.WebViewHandler.apply")
|
|
1232
1932
|
_apply_common(label, merged)
|
|
1233
1933
|
|
|
1234
1934
|
|
|
@@ -1238,7 +1938,16 @@ class WebViewHandler(DesktopViewHandler):
|
|
|
1238
1938
|
|
|
1239
1939
|
|
|
1240
1940
|
class PressableHandler(DesktopViewHandler):
|
|
1241
|
-
"""A frame that forwards press / long-press / gestures.
|
|
1941
|
+
"""A frame that forwards press / long-press / gestures.
|
|
1942
|
+
|
|
1943
|
+
``hit_slop`` support is partial (see ``_within_hit_rect``): the
|
|
1944
|
+
slop insets can't grow the initial press target, but they expand
|
|
1945
|
+
the tracking rect, so a release (or pointer drift during a pending
|
|
1946
|
+
long press) within the slop of the frame's edges still counts as
|
|
1947
|
+
inside. A release outside the expanded rect fires ``on_press_out``
|
|
1948
|
+
without ``on_press``, matching the device backends' cancel-on-exit
|
|
1949
|
+
behavior.
|
|
1950
|
+
"""
|
|
1242
1951
|
|
|
1243
1952
|
def build(self, props: Dict[str, Any]) -> Any:
|
|
1244
1953
|
frame = tk.Frame(_master(), highlightthickness=0, bd=0, cursor="hand2")
|
|
@@ -1251,7 +1960,8 @@ class PressableHandler(DesktopViewHandler):
|
|
|
1251
1960
|
frame._pn_long_fired = False
|
|
1252
1961
|
self._cancel_long(frame)
|
|
1253
1962
|
_fire(frame, "on_press_out")
|
|
1254
|
-
|
|
1963
|
+
inside = event is None or _within_hit_rect(frame, float(event.x), float(event.y))
|
|
1964
|
+
if not fired_long and inside:
|
|
1255
1965
|
_fire(frame, "on_press")
|
|
1256
1966
|
|
|
1257
1967
|
def _on_press_down(_event: Any = None) -> None:
|
|
@@ -1264,15 +1974,21 @@ class PressableHandler(DesktopViewHandler):
|
|
|
1264
1974
|
frame._pn_long_fired = True
|
|
1265
1975
|
_fire(frame, "on_long_press")
|
|
1266
1976
|
|
|
1267
|
-
def
|
|
1268
|
-
|
|
1977
|
+
def _on_drift(event: Any = None) -> None:
|
|
1978
|
+
# A pending long press survives pointer drift inside the
|
|
1979
|
+
# hit rect (frame plus hit_slop). <Leave> fires once at
|
|
1980
|
+
# the edge crossing, so <B1-Motion> covers travel beyond
|
|
1981
|
+
# it; Tk's implicit grab keeps reporting positions.
|
|
1982
|
+
if event is None or not _within_hit_rect(frame, float(event.x), float(event.y)):
|
|
1983
|
+
self._cancel_long(frame)
|
|
1269
1984
|
|
|
1270
1985
|
try:
|
|
1271
1986
|
frame.bind("<ButtonRelease-1>", _on_release, add="+")
|
|
1272
1987
|
frame.bind("<ButtonPress-1>", _on_press_down, add="+")
|
|
1273
|
-
frame.bind("<
|
|
1988
|
+
frame.bind("<B1-Motion>", _on_drift, add="+")
|
|
1989
|
+
frame.bind("<Leave>", _on_drift, add="+")
|
|
1274
1990
|
except Exception:
|
|
1275
|
-
|
|
1991
|
+
diagnostics.swallowed("desktop.PressableHandler._bind")
|
|
1276
1992
|
|
|
1277
1993
|
@staticmethod
|
|
1278
1994
|
def _cancel_long(frame: Any) -> None:
|
|
@@ -1281,7 +1997,7 @@ class PressableHandler(DesktopViewHandler):
|
|
|
1281
1997
|
try:
|
|
1282
1998
|
frame.after_cancel(after_id)
|
|
1283
1999
|
except Exception:
|
|
1284
|
-
|
|
2000
|
+
diagnostics.swallowed("desktop.PressableHandler._cancel_long")
|
|
1285
2001
|
frame._pn_long_after = None
|
|
1286
2002
|
|
|
1287
2003
|
|
|
@@ -1313,7 +2029,7 @@ class ModalHandler(DesktopViewHandler):
|
|
|
1313
2029
|
else:
|
|
1314
2030
|
frame.place_forget()
|
|
1315
2031
|
except Exception:
|
|
1316
|
-
|
|
2032
|
+
diagnostics.swallowed("desktop.ModalHandler.apply")
|
|
1317
2033
|
if visible != getattr(frame, "_pn_was_visible", None):
|
|
1318
2034
|
frame._pn_was_visible = visible
|
|
1319
2035
|
if visible:
|
|
@@ -1357,7 +2073,7 @@ class PortalHandler(DesktopViewHandler):
|
|
|
1357
2073
|
try:
|
|
1358
2074
|
child.place_forget()
|
|
1359
2075
|
except Exception:
|
|
1360
|
-
|
|
2076
|
+
diagnostics.swallowed("desktop.PortalHandler.remove_child")
|
|
1361
2077
|
|
|
1362
2078
|
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1363
2079
|
# The portal frame itself stays unmapped; only children render.
|
|
@@ -1377,7 +2093,7 @@ class TabBarHandler(DesktopViewHandler):
|
|
|
1377
2093
|
try:
|
|
1378
2094
|
frame.configure(highlightbackground="#c6c6c8", highlightcolor="#c6c6c8")
|
|
1379
2095
|
except Exception:
|
|
1380
|
-
|
|
2096
|
+
diagnostics.swallowed("desktop.TabBarHandler.build")
|
|
1381
2097
|
frame._pn_buttons = []
|
|
1382
2098
|
return frame
|
|
1383
2099
|
|
|
@@ -1389,7 +2105,7 @@ class TabBarHandler(DesktopViewHandler):
|
|
|
1389
2105
|
try:
|
|
1390
2106
|
button.destroy()
|
|
1391
2107
|
except Exception:
|
|
1392
|
-
|
|
2108
|
+
diagnostics.swallowed("desktop.TabBarHandler.apply")
|
|
1393
2109
|
buttons: List[Any] = []
|
|
1394
2110
|
for item in items:
|
|
1395
2111
|
name = item.get("name")
|
|
@@ -1428,7 +2144,7 @@ class TabBarHandler(DesktopViewHandler):
|
|
|
1428
2144
|
try:
|
|
1429
2145
|
button.place(x=i * each, y=0, width=each, height=frame_h)
|
|
1430
2146
|
except Exception:
|
|
1431
|
-
|
|
2147
|
+
diagnostics.swallowed("desktop.TabBarHandler._layout_buttons")
|
|
1432
2148
|
|
|
1433
2149
|
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1434
2150
|
native_view._pn_size = (max(0.0, _finite(width)), max(0.0, _finite(height)))
|
|
@@ -1458,7 +2174,7 @@ class PickerHandler(DesktopViewHandler):
|
|
|
1458
2174
|
try:
|
|
1459
2175
|
combo.bind("<<ComboboxSelected>>", _on_select)
|
|
1460
2176
|
except Exception:
|
|
1461
|
-
|
|
2177
|
+
diagnostics.swallowed("desktop.PickerHandler.build")
|
|
1462
2178
|
return combo
|
|
1463
2179
|
|
|
1464
2180
|
def apply(self, combo: Any, props: Dict[str, Any]) -> None:
|
|
@@ -1469,7 +2185,7 @@ class PickerHandler(DesktopViewHandler):
|
|
|
1469
2185
|
try:
|
|
1470
2186
|
combo.configure(values=labels)
|
|
1471
2187
|
except Exception:
|
|
1472
|
-
|
|
2188
|
+
diagnostics.swallowed("desktop.PickerHandler.apply")
|
|
1473
2189
|
if "value" in merged:
|
|
1474
2190
|
target = merged.get("value")
|
|
1475
2191
|
for i, item in enumerate(items):
|
|
@@ -1477,7 +2193,7 @@ class PickerHandler(DesktopViewHandler):
|
|
|
1477
2193
|
try:
|
|
1478
2194
|
combo.current(i)
|
|
1479
2195
|
except Exception:
|
|
1480
|
-
|
|
2196
|
+
diagnostics.swallowed("desktop.PickerHandler.apply")
|
|
1481
2197
|
break
|
|
1482
2198
|
|
|
1483
2199
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
@@ -1499,7 +2215,7 @@ class SegmentedControlHandler(DesktopViewHandler):
|
|
|
1499
2215
|
try:
|
|
1500
2216
|
button.destroy()
|
|
1501
2217
|
except Exception:
|
|
1502
|
-
|
|
2218
|
+
diagnostics.swallowed("desktop.SegmentedControlHandler.apply")
|
|
1503
2219
|
buttons: List[Any] = []
|
|
1504
2220
|
for i, label in enumerate(segments):
|
|
1505
2221
|
is_active = i == selected
|
|
@@ -1522,7 +2238,7 @@ class SegmentedControlHandler(DesktopViewHandler):
|
|
|
1522
2238
|
state = "normal" if merged.get("enabled", True) else "disabled"
|
|
1523
2239
|
button.configure({"highlightbackground": tint, "state": state})
|
|
1524
2240
|
except Exception:
|
|
1525
|
-
|
|
2241
|
+
diagnostics.swallowed("desktop.SegmentedControlHandler.apply")
|
|
1526
2242
|
buttons.append(button)
|
|
1527
2243
|
frame._pn_buttons = buttons
|
|
1528
2244
|
self._layout_buttons(frame)
|
|
@@ -1540,7 +2256,7 @@ class SegmentedControlHandler(DesktopViewHandler):
|
|
|
1540
2256
|
try:
|
|
1541
2257
|
button.place(x=i * each, y=0, width=each, height=frame_h)
|
|
1542
2258
|
except Exception:
|
|
1543
|
-
|
|
2259
|
+
diagnostics.swallowed("desktop.SegmentedControlHandler._layout_buttons")
|
|
1544
2260
|
|
|
1545
2261
|
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1546
2262
|
native_view._pn_size = (max(0.0, _finite(width)), max(0.0, _finite(height)))
|
|
@@ -1560,7 +2276,7 @@ class DatePickerHandler(DesktopViewHandler):
|
|
|
1560
2276
|
try:
|
|
1561
2277
|
entry.bind("<KeyRelease>", _on_key)
|
|
1562
2278
|
except Exception:
|
|
1563
|
-
|
|
2279
|
+
diagnostics.swallowed("desktop.DatePickerHandler.build")
|
|
1564
2280
|
return entry
|
|
1565
2281
|
|
|
1566
2282
|
def apply(self, entry: Any, props: Dict[str, Any]) -> None:
|
|
@@ -1569,7 +2285,7 @@ class DatePickerHandler(DesktopViewHandler):
|
|
|
1569
2285
|
try:
|
|
1570
2286
|
entry.configure(state="normal" if merged.get("enabled", True) else "disabled")
|
|
1571
2287
|
except Exception:
|
|
1572
|
-
|
|
2288
|
+
diagnostics.swallowed("desktop.DatePickerHandler.apply")
|
|
1573
2289
|
if "value" in props:
|
|
1574
2290
|
incoming = "" if props["value"] is None else str(props["value"])
|
|
1575
2291
|
try:
|
|
@@ -1580,11 +2296,11 @@ class DatePickerHandler(DesktopViewHandler):
|
|
|
1580
2296
|
entry.insert(0, incoming)
|
|
1581
2297
|
entry.configure(state=state)
|
|
1582
2298
|
except Exception:
|
|
1583
|
-
|
|
2299
|
+
diagnostics.swallowed("desktop.DatePickerHandler.apply")
|
|
1584
2300
|
try:
|
|
1585
2301
|
entry.configure(highlightbackground="#c7c7cc", highlightcolor="#007aff")
|
|
1586
2302
|
except Exception:
|
|
1587
|
-
|
|
2303
|
+
diagnostics.swallowed("desktop.DatePickerHandler.apply")
|
|
1588
2304
|
|
|
1589
2305
|
|
|
1590
2306
|
# ======================================================================
|
|
@@ -1596,9 +2312,12 @@ def register_handlers(registry: Any) -> None:
|
|
|
1596
2312
|
"""Register every built-in desktop handler on ``registry``.
|
|
1597
2313
|
|
|
1598
2314
|
Mirrors ``register_handlers`` in the iOS / Android backends so the
|
|
1599
|
-
desktop registry services the same element types
|
|
1600
|
-
|
|
1601
|
-
|
|
2315
|
+
desktop registry services the same element types, including
|
|
2316
|
+
``VirtualList``. ``FlatList`` and ``SectionList`` still pick the
|
|
2317
|
+
Python-windowed engine on desktop (``_native_lists_supported`` is
|
|
2318
|
+
false here), but trees that emit ``VirtualList`` directly, and
|
|
2319
|
+
tests that exercise the native-list routing, get the same command
|
|
2320
|
+
and prop contract in the preview.
|
|
1602
2321
|
"""
|
|
1603
2322
|
flex = FlexContainerHandler()
|
|
1604
2323
|
registry.register("View", flex)
|
|
@@ -1626,3 +2345,4 @@ def register_handlers(registry: Any) -> None:
|
|
|
1626
2345
|
registry.register("Checkbox", CheckboxHandler())
|
|
1627
2346
|
registry.register("SegmentedControl", SegmentedControlHandler())
|
|
1628
2347
|
registry.register("DatePicker", DatePickerHandler())
|
|
2348
|
+
registry.register("VirtualList", VirtualListHandler())
|