pythonnative 0.20.0__py3-none-any.whl → 0.22.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 +14 -3
- pythonnative/animated.py +420 -135
- pythonnative/cli/pn.py +450 -956
- pythonnative/components.py +519 -235
- pythonnative/events.py +210 -0
- pythonnative/gestures.py +875 -0
- pythonnative/layout.py +463 -149
- pythonnative/mutations.py +130 -0
- pythonnative/native_views/__init__.py +161 -97
- pythonnative/native_views/android.py +1050 -1124
- pythonnative/native_views/base.py +108 -18
- pythonnative/native_views/desktop.py +460 -417
- pythonnative/native_views/ios.py +1918 -1916
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +504 -0
- pythonnative/project/builder.py +555 -0
- pythonnative/project/config.py +642 -0
- pythonnative/project/doctor.py +233 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +344 -0
- pythonnative/project/permissions.py +343 -0
- pythonnative/project/runtime_assets.py +272 -0
- pythonnative/reconciler.py +540 -470
- pythonnative/screen.py +5 -2
- pythonnative/sdk/_components.py +2 -2
- pythonnative/templates/android_template/app/build.gradle +2 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/METADATA +10 -2
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/RECORD +32 -21
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -129
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/top_level.txt +0 -0
|
@@ -10,28 +10,41 @@ flex engine** in [`pythonnative.layout`][pythonnative.layout]: the
|
|
|
10
10
|
reconciler computes each view's ``(x, y, width, height)`` in points and
|
|
11
11
|
[`set_frame`][pythonnative.native_views.desktop.DesktopViewHandler.set_frame]
|
|
12
12
|
applies it. Handlers therefore only deal with *visual* props (text,
|
|
13
|
-
colors, fonts
|
|
13
|
+
colors, fonts) and ignore everything in
|
|
14
14
|
[`LAYOUT_STYLE_KEYS`][pythonnative.layout.LAYOUT_STYLE_KEYS].
|
|
15
15
|
|
|
16
|
+
Event contract
|
|
17
|
+
--------------
|
|
18
|
+
Handlers never see Python callables. Each view stores its reconciler
|
|
19
|
+
tag (``widget._pn_tag``); Tk callbacks forward through
|
|
20
|
+
[`dispatch_event`][pythonnative.events.dispatch_event] with that tag,
|
|
21
|
+
and the Python-side [`EventRegistry`][pythonnative.events.EventRegistry]
|
|
22
|
+
routes to whatever closure the current render registered. Views with a
|
|
23
|
+
``gestures`` prop feed Tk pointer events into the shared pure-Python
|
|
24
|
+
[`GestureArbiter`][pythonnative.gestures.GestureArbiter].
|
|
25
|
+
|
|
16
26
|
Placement strategy
|
|
17
27
|
------------------
|
|
18
28
|
Tkinter fixes a widget's master at construction time, but the
|
|
19
|
-
reconciler creates
|
|
20
|
-
``
|
|
29
|
+
reconciler creates views before parents (``CreateOp`` before
|
|
30
|
+
``InsertOp``). To bridge that, every widget is created under a single
|
|
21
31
|
shared *stage* frame (see
|
|
22
32
|
[`set_root_container`][pythonnative.native_views.desktop.set_root_container])
|
|
23
33
|
and positioned with ``place(in_=parent, ...)``. Tk's ``-in`` option
|
|
24
34
|
composes coordinates through nested parents, so the engine's
|
|
25
35
|
parent-relative frames render correctly without reparenting.
|
|
36
|
+
ScrollViews shift their children's placement by the current scroll
|
|
37
|
+
offset, which yields real wheel scrolling in the preview.
|
|
26
38
|
|
|
27
39
|
Scope
|
|
28
40
|
-----
|
|
29
41
|
This is a **preview** backend, not a production desktop target. It
|
|
30
42
|
favors fidelity of layout and behavior over pixel-perfect chrome:
|
|
31
43
|
rounded corners, shadows, per-widget opacity, and overflow clipping are
|
|
32
|
-
approximated or omitted (Tkinter can't express them cheaply).
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
approximated or omitted (Tkinter can't express them cheaply). Native
|
|
45
|
+
animation is declined (``start_animation`` returns ``False``), so the
|
|
46
|
+
Python ticker drives previews of animations through
|
|
47
|
+
``set_animated_property``.
|
|
35
48
|
|
|
36
49
|
This module imports ``tkinter`` at import time, so it is only imported
|
|
37
50
|
when ``PN_PLATFORM=desktop``. Off-device unit tests inject a mock
|
|
@@ -43,11 +56,14 @@ from __future__ import annotations
|
|
|
43
56
|
|
|
44
57
|
import math
|
|
45
58
|
import re
|
|
59
|
+
import time
|
|
46
60
|
import tkinter as tk
|
|
47
61
|
from tkinter import font as tkfont
|
|
48
62
|
from tkinter import ttk
|
|
49
63
|
from typing import Any, Dict, List, Optional, Tuple
|
|
50
64
|
|
|
65
|
+
from ..events import dispatch_event, event_names
|
|
66
|
+
from ..gestures import make_arbiter
|
|
51
67
|
from .base import ViewHandler
|
|
52
68
|
|
|
53
69
|
# ======================================================================
|
|
@@ -67,12 +83,14 @@ _DEFAULT_FONT_SIZE = 15
|
|
|
67
83
|
def set_root_container(container: Any) -> None:
|
|
68
84
|
"""Install the stage frame that every desktop view is created under.
|
|
69
85
|
|
|
70
|
-
Called by ``pythonnative.preview`` before the
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
Called by ``pythonnative.preview`` before the first screen is
|
|
87
|
+
mounted. ``container`` must be a Tk widget (a ``Frame`` filling the
|
|
88
|
+
preview window). Also installs the global mouse-wheel binding that
|
|
89
|
+
powers ScrollView scrolling.
|
|
73
90
|
"""
|
|
74
91
|
global _ROOT_CONTAINER
|
|
75
92
|
_ROOT_CONTAINER = container
|
|
93
|
+
_install_wheel_bindings(container)
|
|
76
94
|
|
|
77
95
|
|
|
78
96
|
def get_root_container() -> Any:
|
|
@@ -248,7 +266,25 @@ def _finite(value: Any, default: float = 0.0) -> float:
|
|
|
248
266
|
|
|
249
267
|
|
|
250
268
|
# ======================================================================
|
|
251
|
-
#
|
|
269
|
+
# Event dispatch helpers
|
|
270
|
+
# ======================================================================
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _fire(widget: Any, name: str, *args: Any) -> None:
|
|
274
|
+
"""Dispatch event ``name`` for ``widget`` through the tag registry."""
|
|
275
|
+
tag = getattr(widget, "_pn_tag", None)
|
|
276
|
+
if tag is not None:
|
|
277
|
+
dispatch_event(tag, name, *args)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _has_event(widget: Any, name: str) -> bool:
|
|
281
|
+
"""Whether the element wired a callback named ``name`` this render."""
|
|
282
|
+
merged = getattr(widget, "_pn_props", None) or {}
|
|
283
|
+
return name in event_names(merged)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ======================================================================
|
|
287
|
+
# Placement (ordering-independent, scroll-aware)
|
|
252
288
|
# ======================================================================
|
|
253
289
|
|
|
254
290
|
|
|
@@ -269,10 +305,11 @@ def _place(widget: Any) -> None:
|
|
|
269
305
|
"""Position ``widget`` inside its logical parent, if both are known.
|
|
270
306
|
|
|
271
307
|
Idempotent and order-independent: ``set_frame`` records the frame
|
|
272
|
-
and ``
|
|
273
|
-
the actual ``place``. Coordinates compose through nested
|
|
274
|
-
parents, so a child's parent-relative frame lands at the
|
|
275
|
-
absolute spot.
|
|
308
|
+
and ``insert_child`` records the parent; whichever runs second
|
|
309
|
+
triggers the actual ``place``. Coordinates compose through nested
|
|
310
|
+
``-in`` parents, so a child's parent-relative frame lands at the
|
|
311
|
+
right absolute spot. A scrollable parent shifts every child by its
|
|
312
|
+
current scroll offset.
|
|
276
313
|
"""
|
|
277
314
|
frame = getattr(widget, "_pn_frame", None)
|
|
278
315
|
if frame is None:
|
|
@@ -283,13 +320,32 @@ def _place(widget: Any) -> None:
|
|
|
283
320
|
return
|
|
284
321
|
x, y, w, h = frame
|
|
285
322
|
tx, ty = getattr(widget, "_pn_translate", (0.0, 0.0))
|
|
323
|
+
sx, sy = getattr(target, "_pn_scroll_offset", (0.0, 0.0)) if parent is not None else (0.0, 0.0)
|
|
286
324
|
try:
|
|
287
|
-
widget.place(in_=target, x=x + tx, y=y + ty, width=max(0.0, w), height=max(0.0, h))
|
|
325
|
+
widget.place(in_=target, x=x + tx - sx, y=y + ty - sy, width=max(0.0, w), height=max(0.0, h))
|
|
288
326
|
widget.lift()
|
|
289
327
|
except Exception:
|
|
290
328
|
pass
|
|
291
329
|
|
|
292
330
|
|
|
331
|
+
def _register_child(parent: Any, child: Any, index: int) -> None:
|
|
332
|
+
"""Track ``child`` in ``parent``'s ordered child list at ``index``."""
|
|
333
|
+
children: List[Any] = getattr(parent, "_pn_children", None) or []
|
|
334
|
+
if child in children:
|
|
335
|
+
children.remove(child)
|
|
336
|
+
children.insert(min(max(index, 0), len(children)), child)
|
|
337
|
+
parent._pn_children = children
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _unregister_child(parent: Any, child: Any) -> None:
|
|
341
|
+
children: List[Any] = getattr(parent, "_pn_children", None) or []
|
|
342
|
+
try:
|
|
343
|
+
children.remove(child)
|
|
344
|
+
except ValueError:
|
|
345
|
+
pass
|
|
346
|
+
parent._pn_children = children
|
|
347
|
+
|
|
348
|
+
|
|
293
349
|
def _set_translate_from_transform(widget: Any, spec: Any) -> None:
|
|
294
350
|
"""Extract a translate offset from a ``transform`` prop for placement.
|
|
295
351
|
|
|
@@ -340,34 +396,229 @@ def _apply_common(widget: Any, props: Dict[str, Any]) -> None:
|
|
|
340
396
|
|
|
341
397
|
|
|
342
398
|
# ======================================================================
|
|
343
|
-
#
|
|
399
|
+
# Gesture wiring (pure-Python arbiter over Tk pointer events)
|
|
344
400
|
# ======================================================================
|
|
345
401
|
|
|
346
402
|
|
|
347
|
-
|
|
348
|
-
"""
|
|
403
|
+
def _wire_gestures(widget: Any, specs: Any) -> None:
|
|
404
|
+
"""Feed Tk pointer events on ``widget`` into a `GestureArbiter`.
|
|
349
405
|
|
|
350
|
-
|
|
351
|
-
``
|
|
352
|
-
|
|
353
|
-
|
|
406
|
+
The arbiter emits ``(gesture_index, payload)`` pairs which are
|
|
407
|
+
forwarded as ``gesture:<i>`` events for this widget's tag. Long
|
|
408
|
+
presses use Tk's ``after`` timer to poll the arbiter at its next
|
|
409
|
+
deadline.
|
|
354
410
|
"""
|
|
411
|
+
if not isinstance(specs, (list, tuple)) or not specs:
|
|
412
|
+
widget._pn_arbiter = None
|
|
413
|
+
return
|
|
355
414
|
|
|
356
|
-
def
|
|
357
|
-
|
|
415
|
+
def _emit(index: int, payload: Dict[str, Any]) -> None:
|
|
416
|
+
_fire(widget, f"gesture:{index}", payload)
|
|
417
|
+
|
|
418
|
+
arbiter = make_arbiter([s for s in specs if isinstance(s, dict)], _emit)
|
|
419
|
+
widget._pn_arbiter = arbiter
|
|
420
|
+
if getattr(widget, "_pn_gestures_bound", False):
|
|
421
|
+
return
|
|
422
|
+
widget._pn_gestures_bound = True
|
|
423
|
+
|
|
424
|
+
def _schedule_poll() -> None:
|
|
425
|
+
current = getattr(widget, "_pn_arbiter", None)
|
|
426
|
+
if current is None:
|
|
427
|
+
return
|
|
428
|
+
deadline = current.next_deadline()
|
|
429
|
+
if deadline is None:
|
|
430
|
+
return
|
|
431
|
+
delay_ms = max(1, int((deadline - time.monotonic()) * 1000.0))
|
|
432
|
+
|
|
433
|
+
def _poll() -> None:
|
|
434
|
+
live = getattr(widget, "_pn_arbiter", None)
|
|
435
|
+
if live is not None:
|
|
436
|
+
live.poll(time.monotonic())
|
|
437
|
+
_schedule_poll()
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
widget.after(delay_ms, _poll)
|
|
441
|
+
except Exception:
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
def _on_down(event: Any) -> None:
|
|
445
|
+
current = getattr(widget, "_pn_arbiter", None)
|
|
446
|
+
if current is not None:
|
|
447
|
+
current.pointer_down(0, float(event.x), float(event.y), time.monotonic())
|
|
448
|
+
_schedule_poll()
|
|
449
|
+
|
|
450
|
+
def _on_move(event: Any) -> None:
|
|
451
|
+
current = getattr(widget, "_pn_arbiter", None)
|
|
452
|
+
if current is not None:
|
|
453
|
+
current.pointer_move(0, float(event.x), float(event.y), time.monotonic())
|
|
454
|
+
|
|
455
|
+
def _on_up(event: Any) -> None:
|
|
456
|
+
current = getattr(widget, "_pn_arbiter", None)
|
|
457
|
+
if current is not None:
|
|
458
|
+
current.pointer_up(0, float(event.x), float(event.y), time.monotonic())
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
widget.bind("<ButtonPress-1>", _on_down, add="+")
|
|
462
|
+
widget.bind("<B1-Motion>", _on_move, add="+")
|
|
463
|
+
widget.bind("<ButtonRelease-1>", _on_up, add="+")
|
|
464
|
+
except Exception:
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# ======================================================================
|
|
469
|
+
# ScrollView wheel support
|
|
470
|
+
# ======================================================================
|
|
471
|
+
|
|
472
|
+
_WHEEL_BOUND = False
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _install_wheel_bindings(container: Any) -> None:
|
|
476
|
+
"""Install the global wheel handler that drives preview scrolling.
|
|
477
|
+
|
|
478
|
+
Tk pointer events don't bubble, so a single ``bind_all`` on the
|
|
479
|
+
toplevel hit-tests the widget under the cursor and walks the
|
|
480
|
+
logical ``_pn_parent`` chain to the nearest scrollable ancestor.
|
|
481
|
+
"""
|
|
482
|
+
global _WHEEL_BOUND
|
|
483
|
+
if _WHEEL_BOUND:
|
|
484
|
+
return
|
|
485
|
+
try:
|
|
486
|
+
top = container.winfo_toplevel()
|
|
487
|
+
except Exception:
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
def _on_wheel(event: Any) -> None:
|
|
491
|
+
delta = getattr(event, "delta", 0)
|
|
492
|
+
if getattr(event, "num", None) == 4:
|
|
493
|
+
delta = 120
|
|
494
|
+
elif getattr(event, "num", None) == 5:
|
|
495
|
+
delta = -120
|
|
496
|
+
if not delta:
|
|
497
|
+
return
|
|
498
|
+
try:
|
|
499
|
+
under = event.widget.winfo_containing(event.x_root, event.y_root)
|
|
500
|
+
except Exception:
|
|
501
|
+
under = None
|
|
502
|
+
node = under
|
|
503
|
+
while node is not None:
|
|
504
|
+
if getattr(node, "_pn_scrollable", False):
|
|
505
|
+
_scroll_by(node, delta)
|
|
506
|
+
return
|
|
507
|
+
node = getattr(node, "_pn_parent", None)
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
top.bind_all("<MouseWheel>", _on_wheel, add="+")
|
|
511
|
+
top.bind_all("<Button-4>", _on_wheel, add="+")
|
|
512
|
+
top.bind_all("<Button-5>", _on_wheel, add="+")
|
|
513
|
+
_WHEEL_BOUND = True
|
|
514
|
+
except Exception:
|
|
515
|
+
pass
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _content_extent(widget: Any) -> Tuple[float, float]:
|
|
519
|
+
"""Max (right, bottom) edge over the scroll container's children."""
|
|
520
|
+
max_x = 0.0
|
|
521
|
+
max_y = 0.0
|
|
522
|
+
for child in getattr(widget, "_pn_children", []) or []:
|
|
523
|
+
frame = getattr(child, "_pn_frame", None)
|
|
524
|
+
if frame is None:
|
|
525
|
+
continue
|
|
526
|
+
x, y, w, h = frame
|
|
527
|
+
max_x = max(max_x, x + w)
|
|
528
|
+
max_y = max(max_y, y + h)
|
|
529
|
+
return (max_x, max_y)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _scroll_to(widget: Any, x: float, y: float, fire_event: bool = True) -> None:
|
|
533
|
+
"""Set a scroll container's offset, clamped to its content extent."""
|
|
534
|
+
frame = getattr(widget, "_pn_frame", None)
|
|
535
|
+
vw = frame[2] if frame else 0.0
|
|
536
|
+
vh = frame[3] if frame else 0.0
|
|
537
|
+
content_w, content_h = _content_extent(widget)
|
|
538
|
+
max_x = max(0.0, content_w - vw)
|
|
539
|
+
max_y = max(0.0, content_h - vh)
|
|
540
|
+
new_offset = (min(max(0.0, x), max_x), min(max(0.0, y), max_y))
|
|
541
|
+
if new_offset == getattr(widget, "_pn_scroll_offset", (0.0, 0.0)):
|
|
542
|
+
return
|
|
543
|
+
widget._pn_scroll_offset = new_offset
|
|
544
|
+
for child in getattr(widget, "_pn_children", []) or []:
|
|
358
545
|
_place(child)
|
|
546
|
+
if fire_event:
|
|
547
|
+
_fire(widget, "on_scroll", {"x": new_offset[0], "y": new_offset[1]})
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _scroll_by(widget: Any, wheel_delta: float) -> None:
|
|
551
|
+
sx, sy = getattr(widget, "_pn_scroll_offset", (0.0, 0.0))
|
|
552
|
+
step = -wheel_delta # natural direction: wheel up scrolls content up
|
|
553
|
+
horizontal = (getattr(widget, "_pn_props", {}) or {}).get("scroll_axis") == "horizontal"
|
|
554
|
+
if horizontal:
|
|
555
|
+
_scroll_to(widget, sx + step, sy)
|
|
556
|
+
else:
|
|
557
|
+
_scroll_to(widget, sx, sy + step)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# ======================================================================
|
|
561
|
+
# Base handler
|
|
562
|
+
# ======================================================================
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
class DesktopViewHandler(ViewHandler):
|
|
566
|
+
"""Shared create/update/frame/child behavior for Tk handlers.
|
|
567
|
+
|
|
568
|
+
Concrete handlers implement
|
|
569
|
+
[`build`][pythonnative.native_views.desktop.DesktopViewHandler.build]
|
|
570
|
+
(construct the widget) and optionally
|
|
571
|
+
[`apply`][pythonnative.native_views.desktop.DesktopViewHandler.apply]
|
|
572
|
+
(apply visual props); creation bookkeeping (tag stamping, prop
|
|
573
|
+
merging, gesture wiring) is inherited.
|
|
574
|
+
"""
|
|
575
|
+
|
|
576
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
577
|
+
"""Construct and return the bare Tk widget."""
|
|
578
|
+
return tk.Frame(_master(), highlightthickness=0, bd=0)
|
|
579
|
+
|
|
580
|
+
def apply(self, widget: Any, props: Dict[str, Any]) -> None:
|
|
581
|
+
"""Apply changed visual props (`_merge_props` has already run)."""
|
|
582
|
+
_apply_common(widget, getattr(widget, "_pn_props", props))
|
|
583
|
+
|
|
584
|
+
def create(self, tag: int, props: Dict[str, Any]) -> Any:
|
|
585
|
+
widget = self.build(props)
|
|
586
|
+
widget._pn_tag = tag
|
|
587
|
+
_merge_props(widget, props)
|
|
588
|
+
self.apply(widget, props)
|
|
589
|
+
if "gestures" in props:
|
|
590
|
+
_wire_gestures(widget, props.get("gestures"))
|
|
591
|
+
return widget
|
|
592
|
+
|
|
593
|
+
def update(self, native_view: Any, changed_props: Dict[str, Any]) -> None:
|
|
594
|
+
_merge_props(native_view, changed_props)
|
|
595
|
+
self.apply(native_view, changed_props)
|
|
596
|
+
if "gestures" in changed_props:
|
|
597
|
+
_wire_gestures(native_view, changed_props.get("gestures"))
|
|
359
598
|
|
|
360
599
|
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
361
600
|
child._pn_parent = parent
|
|
601
|
+
_register_child(parent, child, index)
|
|
362
602
|
_place(child)
|
|
363
603
|
|
|
364
604
|
def remove_child(self, parent: Any, child: Any) -> None:
|
|
605
|
+
_unregister_child(parent, child)
|
|
365
606
|
try:
|
|
366
607
|
child.place_forget()
|
|
367
608
|
except Exception:
|
|
368
609
|
pass
|
|
369
610
|
child._pn_parent = None
|
|
370
611
|
|
|
612
|
+
def destroy(self, native_view: Any) -> None:
|
|
613
|
+
parent = getattr(native_view, "_pn_parent", None)
|
|
614
|
+
if parent is not None:
|
|
615
|
+
_unregister_child(parent, native_view)
|
|
616
|
+
native_view._pn_arbiter = None
|
|
617
|
+
try:
|
|
618
|
+
native_view.destroy()
|
|
619
|
+
except Exception:
|
|
620
|
+
pass
|
|
621
|
+
|
|
371
622
|
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
372
623
|
if native_view is None:
|
|
373
624
|
return
|
|
@@ -377,19 +628,12 @@ class DesktopViewHandler(ViewHandler):
|
|
|
377
628
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
378
629
|
return (0.0, 0.0)
|
|
379
630
|
|
|
380
|
-
def set_animated_property(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
easing: str = "linear",
|
|
387
|
-
) -> None:
|
|
388
|
-
"""Apply the final value of an animated property (no tween).
|
|
389
|
-
|
|
390
|
-
The preview shows animation *end states* rather than smooth
|
|
391
|
-
interpolation. Translation maps onto placement; opacity, scale,
|
|
392
|
-
and rotation have no cheap Tk analogue and are skipped.
|
|
631
|
+
def set_animated_property(self, native_view: Any, prop_name: str, value: Any) -> None:
|
|
632
|
+
"""Apply one frame of a Python-driven animation.
|
|
633
|
+
|
|
634
|
+
Translation maps onto placement; background color maps onto
|
|
635
|
+
``configure``. Opacity, scale, and rotation have no cheap Tk
|
|
636
|
+
analogue and are skipped (a documented preview limitation).
|
|
393
637
|
"""
|
|
394
638
|
if native_view is None:
|
|
395
639
|
return
|
|
@@ -411,7 +655,7 @@ class DesktopViewHandler(ViewHandler):
|
|
|
411
655
|
|
|
412
656
|
|
|
413
657
|
# ======================================================================
|
|
414
|
-
# Containers (View /
|
|
658
|
+
# Containers (View / SafeAreaView / KeyboardAvoidingView)
|
|
415
659
|
# ======================================================================
|
|
416
660
|
|
|
417
661
|
|
|
@@ -423,30 +667,58 @@ class FlexContainerHandler(DesktopViewHandler):
|
|
|
423
667
|
border).
|
|
424
668
|
"""
|
|
425
669
|
|
|
426
|
-
def create(self, props: Dict[str, Any]) -> Any:
|
|
427
|
-
frame = tk.Frame(_master(), highlightthickness=0, bd=0)
|
|
428
|
-
_apply_common(frame, _merge_props(frame, props))
|
|
429
|
-
return frame
|
|
430
670
|
|
|
431
|
-
|
|
432
|
-
|
|
671
|
+
class SafeAreaViewHandler(FlexContainerHandler):
|
|
672
|
+
"""Desktop has no notch/home-indicator insets, so this is a frame."""
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class KeyboardAvoidingViewHandler(FlexContainerHandler):
|
|
676
|
+
"""No soft keyboard on desktop; behaves as a plain frame."""
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
# ======================================================================
|
|
680
|
+
# ScrollView
|
|
681
|
+
# ======================================================================
|
|
433
682
|
|
|
434
683
|
|
|
435
684
|
class ScrollViewHandler(FlexContainerHandler):
|
|
436
|
-
"""Preview ScrollView
|
|
685
|
+
"""Preview ScrollView with real wheel scrolling.
|
|
437
686
|
|
|
438
|
-
The layout engine
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
687
|
+
The layout engine lets the content grow past the viewport on the
|
|
688
|
+
scroll axis; this handler offsets its children's placement by the
|
|
689
|
+
current scroll offset (overflow outside the frame is *not* clipped
|
|
690
|
+
— a documented preview limitation of the single-stage design).
|
|
442
691
|
|
|
692
|
+
Commands:
|
|
693
|
+
``scroll_to_offset(x=…, y=…)``: jump to an offset.
|
|
694
|
+
``scroll_to_end()``: jump to the end of the content.
|
|
695
|
+
"""
|
|
443
696
|
|
|
444
|
-
|
|
445
|
-
|
|
697
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
698
|
+
frame = tk.Frame(_master(), highlightthickness=0, bd=0)
|
|
699
|
+
frame._pn_scrollable = True
|
|
700
|
+
frame._pn_scroll_offset = (0.0, 0.0)
|
|
701
|
+
return frame
|
|
446
702
|
|
|
703
|
+
def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
704
|
+
if name == "scroll_to_offset":
|
|
705
|
+
sx, sy = getattr(native_view, "_pn_scroll_offset", (0.0, 0.0))
|
|
706
|
+
_scroll_to(native_view, _finite(args.get("x", sx)), _finite(args.get("y", sy)))
|
|
707
|
+
return True
|
|
708
|
+
if name == "scroll_to_end":
|
|
709
|
+
content_w, content_h = _content_extent(native_view)
|
|
710
|
+
_scroll_to(native_view, content_w, content_h)
|
|
711
|
+
return True
|
|
712
|
+
if name == "get_scroll_offset":
|
|
713
|
+
sx, sy = getattr(native_view, "_pn_scroll_offset", (0.0, 0.0))
|
|
714
|
+
return {"x": sx, "y": sy}
|
|
715
|
+
return None
|
|
447
716
|
|
|
448
|
-
|
|
449
|
-
|
|
717
|
+
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
718
|
+
super().set_frame(native_view, x, y, width, height)
|
|
719
|
+
# Keep the offset clamped when the viewport grows.
|
|
720
|
+
sx, sy = getattr(native_view, "_pn_scroll_offset", (0.0, 0.0))
|
|
721
|
+
_scroll_to(native_view, sx, sy, fire_event=False)
|
|
450
722
|
|
|
451
723
|
|
|
452
724
|
# ======================================================================
|
|
@@ -459,16 +731,11 @@ _JUSTIFY_FOR_ALIGN = {"left": "left", "center": "center", "right": "right"}
|
|
|
459
731
|
|
|
460
732
|
|
|
461
733
|
class TextHandler(DesktopViewHandler):
|
|
462
|
-
def
|
|
463
|
-
|
|
464
|
-
self._apply(label, props)
|
|
465
|
-
return label
|
|
466
|
-
|
|
467
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
468
|
-
self._apply(native_view, changed)
|
|
734
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
735
|
+
return tk.Label(_master(), highlightthickness=0, bd=0, padx=0, pady=0)
|
|
469
736
|
|
|
470
|
-
def
|
|
471
|
-
merged =
|
|
737
|
+
def apply(self, label: Any, props: Dict[str, Any]) -> None:
|
|
738
|
+
merged = getattr(label, "_pn_props", props)
|
|
472
739
|
text = merged.get("text")
|
|
473
740
|
label._pn_text = "" if text is None else str(text)
|
|
474
741
|
font = _make_font(merged)
|
|
@@ -513,16 +780,13 @@ class TextHandler(DesktopViewHandler):
|
|
|
513
780
|
|
|
514
781
|
|
|
515
782
|
class ButtonHandler(DesktopViewHandler):
|
|
516
|
-
def
|
|
783
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
517
784
|
button = tk.Button(_master(), highlightthickness=0, takefocus=0)
|
|
518
|
-
|
|
785
|
+
button.configure(command=lambda: _fire(button, "on_click"))
|
|
519
786
|
return button
|
|
520
787
|
|
|
521
|
-
def
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
def _apply(self, button: Any, props: Dict[str, Any]) -> None:
|
|
525
|
-
merged = _merge_props(button, props)
|
|
788
|
+
def apply(self, button: Any, props: Dict[str, Any]) -> None:
|
|
789
|
+
merged = getattr(button, "_pn_props", props)
|
|
526
790
|
title = merged.get("title")
|
|
527
791
|
button._pn_text = "" if title is None else str(title)
|
|
528
792
|
font = _make_font(merged)
|
|
@@ -541,20 +805,6 @@ class ButtonHandler(DesktopViewHandler):
|
|
|
541
805
|
button.configure(**opts)
|
|
542
806
|
except Exception:
|
|
543
807
|
pass
|
|
544
|
-
if "on_click" in props:
|
|
545
|
-
callback = props["on_click"]
|
|
546
|
-
|
|
547
|
-
def _command() -> None:
|
|
548
|
-
if callable(callback):
|
|
549
|
-
try:
|
|
550
|
-
callback()
|
|
551
|
-
except Exception:
|
|
552
|
-
pass
|
|
553
|
-
|
|
554
|
-
try:
|
|
555
|
-
button.configure(command=_command if callable(callback) else "")
|
|
556
|
-
except Exception:
|
|
557
|
-
pass
|
|
558
808
|
_apply_common(button, merged)
|
|
559
809
|
|
|
560
810
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
@@ -572,7 +822,7 @@ class ButtonHandler(DesktopViewHandler):
|
|
|
572
822
|
|
|
573
823
|
|
|
574
824
|
class TextInputHandler(DesktopViewHandler):
|
|
575
|
-
def
|
|
825
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
576
826
|
multiline = bool(props.get("multiline"))
|
|
577
827
|
widget: Any
|
|
578
828
|
if multiline:
|
|
@@ -581,13 +831,9 @@ class TextInputHandler(DesktopViewHandler):
|
|
|
581
831
|
widget = tk.Entry(_master(), highlightthickness=1, bd=0)
|
|
582
832
|
widget._pn_multiline = multiline
|
|
583
833
|
widget._pn_suppress = False
|
|
584
|
-
self._bind(widget
|
|
585
|
-
self._apply(widget, props)
|
|
834
|
+
self._bind(widget)
|
|
586
835
|
return widget
|
|
587
836
|
|
|
588
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
589
|
-
self._apply(native_view, changed)
|
|
590
|
-
|
|
591
837
|
def _current_text(self, widget: Any) -> str:
|
|
592
838
|
try:
|
|
593
839
|
if getattr(widget, "_pn_multiline", False):
|
|
@@ -610,39 +856,33 @@ class TextInputHandler(DesktopViewHandler):
|
|
|
610
856
|
finally:
|
|
611
857
|
widget._pn_suppress = False
|
|
612
858
|
|
|
613
|
-
def _bind(self, widget: Any
|
|
859
|
+
def _bind(self, widget: Any) -> None:
|
|
614
860
|
def _on_key(_event: Any = None) -> None:
|
|
615
861
|
if getattr(widget, "_pn_suppress", False):
|
|
616
862
|
return
|
|
617
|
-
|
|
618
|
-
if callable(callback):
|
|
619
|
-
try:
|
|
620
|
-
callback(self._current_text(widget))
|
|
621
|
-
except Exception:
|
|
622
|
-
pass
|
|
863
|
+
_fire(widget, "on_change", self._current_text(widget))
|
|
623
864
|
|
|
624
865
|
def _on_return(_event: Any = None) -> str:
|
|
625
|
-
|
|
626
|
-
if callable(callback):
|
|
627
|
-
try:
|
|
628
|
-
callback(self._current_text(widget))
|
|
629
|
-
except Exception:
|
|
630
|
-
pass
|
|
866
|
+
_fire(widget, "on_submit", self._current_text(widget))
|
|
631
867
|
return "break"
|
|
632
868
|
|
|
869
|
+
def _on_focus(_event: Any = None) -> None:
|
|
870
|
+
_fire(widget, "on_focus")
|
|
871
|
+
|
|
872
|
+
def _on_blur(_event: Any = None) -> None:
|
|
873
|
+
_fire(widget, "on_blur")
|
|
874
|
+
|
|
633
875
|
try:
|
|
634
876
|
widget.bind("<KeyRelease>", _on_key)
|
|
877
|
+
widget.bind("<FocusIn>", _on_focus)
|
|
878
|
+
widget.bind("<FocusOut>", _on_blur)
|
|
635
879
|
if not getattr(widget, "_pn_multiline", False):
|
|
636
880
|
widget.bind("<Return>", _on_return)
|
|
637
881
|
except Exception:
|
|
638
882
|
pass
|
|
639
883
|
|
|
640
|
-
def
|
|
641
|
-
merged =
|
|
642
|
-
if "on_change" in props:
|
|
643
|
-
widget._pn_on_change = props["on_change"]
|
|
644
|
-
if "on_submit" in props:
|
|
645
|
-
widget._pn_on_submit = props["on_submit"]
|
|
884
|
+
def apply(self, widget: Any, props: Dict[str, Any]) -> None:
|
|
885
|
+
merged = getattr(widget, "_pn_props", props)
|
|
646
886
|
opts: Dict[str, Any] = {"font": _make_font(merged)}
|
|
647
887
|
color = _tk_color(merged.get("color"))
|
|
648
888
|
if color is not None:
|
|
@@ -668,6 +908,21 @@ class TextInputHandler(DesktopViewHandler):
|
|
|
668
908
|
except Exception:
|
|
669
909
|
pass
|
|
670
910
|
|
|
911
|
+
def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
912
|
+
if name == "focus":
|
|
913
|
+
try:
|
|
914
|
+
native_view.focus_set()
|
|
915
|
+
except Exception:
|
|
916
|
+
pass
|
|
917
|
+
return True
|
|
918
|
+
if name == "blur":
|
|
919
|
+
try:
|
|
920
|
+
native_view.winfo_toplevel().focus_set()
|
|
921
|
+
except Exception:
|
|
922
|
+
pass
|
|
923
|
+
return True
|
|
924
|
+
return None
|
|
925
|
+
|
|
671
926
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
672
927
|
merged = getattr(native_view, "_pn_props", {}) or {}
|
|
673
928
|
font = _make_font(merged)
|
|
@@ -692,16 +947,11 @@ class ImageHandler(DesktopViewHandler):
|
|
|
692
947
|
``PhotoImage`` (Tk garbage-collects images that aren't referenced).
|
|
693
948
|
"""
|
|
694
949
|
|
|
695
|
-
def
|
|
696
|
-
|
|
697
|
-
self._apply(label, props)
|
|
698
|
-
return label
|
|
699
|
-
|
|
700
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
701
|
-
self._apply(native_view, changed)
|
|
950
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
951
|
+
return tk.Label(_master(), highlightthickness=0, bd=0, background="#d1d1d6")
|
|
702
952
|
|
|
703
|
-
def
|
|
704
|
-
merged =
|
|
953
|
+
def apply(self, label: Any, props: Dict[str, Any]) -> None:
|
|
954
|
+
merged = getattr(label, "_pn_props", props)
|
|
705
955
|
if "source" in props:
|
|
706
956
|
source = props.get("source")
|
|
707
957
|
photo = None
|
|
@@ -737,35 +987,15 @@ class ImageHandler(DesktopViewHandler):
|
|
|
737
987
|
|
|
738
988
|
|
|
739
989
|
class SwitchHandler(DesktopViewHandler):
|
|
740
|
-
def
|
|
990
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
741
991
|
var = tk.IntVar(master=_master(), value=1 if props.get("value") else 0)
|
|
742
992
|
check = tk.Checkbutton(_master(), variable=var, takefocus=0, highlightthickness=0, text="")
|
|
743
993
|
check._pn_var = var
|
|
744
|
-
|
|
745
|
-
self._apply(check, props)
|
|
994
|
+
check.configure(command=lambda: _fire(check, "on_change", bool(var.get())))
|
|
746
995
|
return check
|
|
747
996
|
|
|
748
|
-
def
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
def _bind(self, check: Any, props: Dict[str, Any]) -> None:
|
|
752
|
-
def _command() -> None:
|
|
753
|
-
callback = getattr(check, "_pn_on_change", None)
|
|
754
|
-
if callable(callback):
|
|
755
|
-
try:
|
|
756
|
-
callback(bool(check._pn_var.get()))
|
|
757
|
-
except Exception:
|
|
758
|
-
pass
|
|
759
|
-
|
|
760
|
-
try:
|
|
761
|
-
check.configure(command=_command)
|
|
762
|
-
except Exception:
|
|
763
|
-
pass
|
|
764
|
-
|
|
765
|
-
def _apply(self, check: Any, props: Dict[str, Any]) -> None:
|
|
766
|
-
merged = _merge_props(check, props)
|
|
767
|
-
if "on_change" in props:
|
|
768
|
-
check._pn_on_change = props["on_change"]
|
|
997
|
+
def apply(self, check: Any, props: Dict[str, Any]) -> None:
|
|
998
|
+
merged = getattr(check, "_pn_props", props)
|
|
769
999
|
if "value" in props:
|
|
770
1000
|
try:
|
|
771
1001
|
check._pn_var.set(1 if props.get("value") else 0)
|
|
@@ -778,35 +1008,15 @@ class SwitchHandler(DesktopViewHandler):
|
|
|
778
1008
|
|
|
779
1009
|
|
|
780
1010
|
class CheckboxHandler(DesktopViewHandler):
|
|
781
|
-
def
|
|
1011
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
782
1012
|
var = tk.IntVar(master=_master(), value=1 if props.get("value") else 0)
|
|
783
1013
|
check = tk.Checkbutton(_master(), variable=var, takefocus=0, highlightthickness=0, anchor="w")
|
|
784
1014
|
check._pn_var = var
|
|
785
|
-
|
|
786
|
-
self._apply(check, props)
|
|
1015
|
+
check.configure(command=lambda: _fire(check, "on_change", bool(var.get())))
|
|
787
1016
|
return check
|
|
788
1017
|
|
|
789
|
-
def
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
def _bind(self, check: Any, props: Dict[str, Any]) -> None:
|
|
793
|
-
def _command() -> None:
|
|
794
|
-
callback = getattr(check, "_pn_on_change", None)
|
|
795
|
-
if callable(callback):
|
|
796
|
-
try:
|
|
797
|
-
callback(bool(check._pn_var.get()))
|
|
798
|
-
except Exception:
|
|
799
|
-
pass
|
|
800
|
-
|
|
801
|
-
try:
|
|
802
|
-
check.configure(command=_command)
|
|
803
|
-
except Exception:
|
|
804
|
-
pass
|
|
805
|
-
|
|
806
|
-
def _apply(self, check: Any, props: Dict[str, Any]) -> None:
|
|
807
|
-
merged = _merge_props(check, props)
|
|
808
|
-
if "on_change" in props:
|
|
809
|
-
check._pn_on_change = props["on_change"]
|
|
1018
|
+
def apply(self, check: Any, props: Dict[str, Any]) -> None:
|
|
1019
|
+
merged = getattr(check, "_pn_props", props)
|
|
810
1020
|
opts: Dict[str, Any] = {}
|
|
811
1021
|
if "label" in merged:
|
|
812
1022
|
opts["text"] = "" if merged.get("label") is None else str(merged["label"])
|
|
@@ -834,7 +1044,7 @@ class CheckboxHandler(DesktopViewHandler):
|
|
|
834
1044
|
|
|
835
1045
|
|
|
836
1046
|
class SliderHandler(DesktopViewHandler):
|
|
837
|
-
def
|
|
1047
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
838
1048
|
scale = tk.Scale(
|
|
839
1049
|
_master(),
|
|
840
1050
|
orient="horizontal",
|
|
@@ -843,31 +1053,19 @@ class SliderHandler(DesktopViewHandler):
|
|
|
843
1053
|
bd=0,
|
|
844
1054
|
sliderlength=20,
|
|
845
1055
|
)
|
|
846
|
-
self._bind(scale, props)
|
|
847
|
-
self._apply(scale, props)
|
|
848
|
-
return scale
|
|
849
1056
|
|
|
850
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
851
|
-
self._apply(native_view, changed)
|
|
852
|
-
|
|
853
|
-
def _bind(self, scale: Any, props: Dict[str, Any]) -> None:
|
|
854
1057
|
def _command(_value: Any) -> None:
|
|
855
|
-
|
|
856
|
-
if callable(callback):
|
|
1058
|
+
if not getattr(scale, "_pn_suppress", False):
|
|
857
1059
|
try:
|
|
858
|
-
|
|
1060
|
+
_fire(scale, "on_change", float(scale.get()))
|
|
859
1061
|
except Exception:
|
|
860
1062
|
pass
|
|
861
1063
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
except Exception:
|
|
865
|
-
pass
|
|
1064
|
+
scale.configure(command=_command)
|
|
1065
|
+
return scale
|
|
866
1066
|
|
|
867
|
-
def
|
|
868
|
-
merged =
|
|
869
|
-
if "on_change" in props:
|
|
870
|
-
scale._pn_on_change = props["on_change"]
|
|
1067
|
+
def apply(self, scale: Any, props: Dict[str, Any]) -> None:
|
|
1068
|
+
merged = getattr(scale, "_pn_props", props)
|
|
871
1069
|
opts: Dict[str, Any] = {
|
|
872
1070
|
"from_": _finite(merged.get("min_value", 0.0)),
|
|
873
1071
|
"to": _finite(merged.get("max_value", 1.0)),
|
|
@@ -894,16 +1092,11 @@ class SliderHandler(DesktopViewHandler):
|
|
|
894
1092
|
|
|
895
1093
|
|
|
896
1094
|
class ProgressBarHandler(DesktopViewHandler):
|
|
897
|
-
def
|
|
898
|
-
|
|
899
|
-
self._apply(bar, props)
|
|
900
|
-
return bar
|
|
1095
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1096
|
+
return ttk.Progressbar(_master(), orient="horizontal", maximum=1.0)
|
|
901
1097
|
|
|
902
|
-
def
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
def _apply(self, bar: Any, props: Dict[str, Any]) -> None:
|
|
906
|
-
merged = _merge_props(bar, props)
|
|
1098
|
+
def apply(self, bar: Any, props: Dict[str, Any]) -> None:
|
|
1099
|
+
merged = getattr(bar, "_pn_props", props)
|
|
907
1100
|
if merged.get("indeterminate"):
|
|
908
1101
|
try:
|
|
909
1102
|
bar.configure(mode="indeterminate")
|
|
@@ -922,16 +1115,11 @@ class ProgressBarHandler(DesktopViewHandler):
|
|
|
922
1115
|
|
|
923
1116
|
|
|
924
1117
|
class ActivityIndicatorHandler(DesktopViewHandler):
|
|
925
|
-
def
|
|
926
|
-
|
|
927
|
-
self._apply(bar, props)
|
|
928
|
-
return bar
|
|
929
|
-
|
|
930
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
931
|
-
self._apply(native_view, changed)
|
|
1118
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1119
|
+
return ttk.Progressbar(_master(), orient="horizontal", mode="indeterminate", length=40)
|
|
932
1120
|
|
|
933
|
-
def
|
|
934
|
-
merged =
|
|
1121
|
+
def apply(self, bar: Any, props: Dict[str, Any]) -> None:
|
|
1122
|
+
merged = getattr(bar, "_pn_props", props)
|
|
935
1123
|
try:
|
|
936
1124
|
if merged.get("animating", True):
|
|
937
1125
|
bar.start(50)
|
|
@@ -952,42 +1140,31 @@ class ActivityIndicatorHandler(DesktopViewHandler):
|
|
|
952
1140
|
|
|
953
1141
|
|
|
954
1142
|
class SpacerHandler(DesktopViewHandler):
|
|
955
|
-
def
|
|
956
|
-
return tk.Frame(_master(), highlightthickness=0, bd=0)
|
|
957
|
-
|
|
958
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1143
|
+
def apply(self, widget: Any, props: Dict[str, Any]) -> None:
|
|
959
1144
|
pass
|
|
960
1145
|
|
|
961
1146
|
|
|
962
1147
|
class StatusBarHandler(DesktopViewHandler):
|
|
963
1148
|
"""Desktop has no system status bar; render an inert zero-size frame."""
|
|
964
1149
|
|
|
965
|
-
def
|
|
966
|
-
return tk.Frame(_master(), highlightthickness=0, bd=0)
|
|
967
|
-
|
|
968
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1150
|
+
def apply(self, widget: Any, props: Dict[str, Any]) -> None:
|
|
969
1151
|
pass
|
|
970
1152
|
|
|
971
1153
|
|
|
972
1154
|
class WebViewHandler(DesktopViewHandler):
|
|
973
1155
|
"""No embedded browser on desktop; show a labeled placeholder."""
|
|
974
1156
|
|
|
975
|
-
def
|
|
976
|
-
|
|
1157
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1158
|
+
return tk.Label(
|
|
977
1159
|
_master(),
|
|
978
1160
|
background="#1c1c1e",
|
|
979
1161
|
foreground="#ffffff",
|
|
980
1162
|
highlightthickness=0,
|
|
981
1163
|
justify="center",
|
|
982
1164
|
)
|
|
983
|
-
self._apply(label, props)
|
|
984
|
-
return label
|
|
985
|
-
|
|
986
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
987
|
-
self._apply(native_view, changed)
|
|
988
1165
|
|
|
989
|
-
def
|
|
990
|
-
merged =
|
|
1166
|
+
def apply(self, label: Any, props: Dict[str, Any]) -> None:
|
|
1167
|
+
merged = getattr(label, "_pn_props", props)
|
|
991
1168
|
target = merged.get("url") or ("inline HTML" if merged.get("html") else "")
|
|
992
1169
|
try:
|
|
993
1170
|
label.configure(text=f"\U0001f310 WebView\n{target}")
|
|
@@ -1002,62 +1179,51 @@ class WebViewHandler(DesktopViewHandler):
|
|
|
1002
1179
|
|
|
1003
1180
|
|
|
1004
1181
|
class PressableHandler(DesktopViewHandler):
|
|
1005
|
-
"""A frame that forwards
|
|
1182
|
+
"""A frame that forwards press / long-press / gestures."""
|
|
1006
1183
|
|
|
1007
|
-
def
|
|
1184
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1008
1185
|
frame = tk.Frame(_master(), highlightthickness=0, bd=0, cursor="hand2")
|
|
1009
1186
|
self._bind(frame)
|
|
1010
|
-
self._apply(frame, props)
|
|
1011
1187
|
return frame
|
|
1012
1188
|
|
|
1013
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1014
|
-
self._apply(native_view, changed)
|
|
1015
|
-
|
|
1016
1189
|
def _bind(self, frame: Any) -> None:
|
|
1017
|
-
def
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
def
|
|
1026
|
-
|
|
1027
|
-
|
|
1190
|
+
def _on_release(event: Any = None) -> None:
|
|
1191
|
+
fired_long = getattr(frame, "_pn_long_fired", False)
|
|
1192
|
+
frame._pn_long_fired = False
|
|
1193
|
+
self._cancel_long(frame)
|
|
1194
|
+
_fire(frame, "on_press_out")
|
|
1195
|
+
if not fired_long:
|
|
1196
|
+
_fire(frame, "on_press")
|
|
1197
|
+
|
|
1198
|
+
def _on_press_down(_event: Any = None) -> None:
|
|
1199
|
+
frame._pn_long_fired = False
|
|
1200
|
+
_fire(frame, "on_press_in")
|
|
1201
|
+
if _has_event(frame, "on_long_press"):
|
|
1028
1202
|
frame._pn_long_after = frame.after(500, _fire_long)
|
|
1029
1203
|
|
|
1030
1204
|
def _fire_long() -> None:
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
try:
|
|
1034
|
-
callback()
|
|
1035
|
-
except Exception:
|
|
1036
|
-
pass
|
|
1205
|
+
frame._pn_long_fired = True
|
|
1206
|
+
_fire(frame, "on_long_press")
|
|
1037
1207
|
|
|
1038
|
-
def
|
|
1039
|
-
|
|
1040
|
-
if after_id is not None:
|
|
1041
|
-
try:
|
|
1042
|
-
frame.after_cancel(after_id)
|
|
1043
|
-
except Exception:
|
|
1044
|
-
pass
|
|
1045
|
-
frame._pn_long_after = None
|
|
1208
|
+
def _on_leave(_event: Any = None) -> None:
|
|
1209
|
+
self._cancel_long(frame)
|
|
1046
1210
|
|
|
1047
1211
|
try:
|
|
1048
|
-
frame.bind("<ButtonRelease-1>",
|
|
1049
|
-
frame.bind("<ButtonPress-1>",
|
|
1050
|
-
frame.bind("<Leave>",
|
|
1212
|
+
frame.bind("<ButtonRelease-1>", _on_release, add="+")
|
|
1213
|
+
frame.bind("<ButtonPress-1>", _on_press_down, add="+")
|
|
1214
|
+
frame.bind("<Leave>", _on_leave, add="+")
|
|
1051
1215
|
except Exception:
|
|
1052
1216
|
pass
|
|
1053
1217
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1218
|
+
@staticmethod
|
|
1219
|
+
def _cancel_long(frame: Any) -> None:
|
|
1220
|
+
after_id = getattr(frame, "_pn_long_after", None)
|
|
1221
|
+
if after_id is not None:
|
|
1222
|
+
try:
|
|
1223
|
+
frame.after_cancel(after_id)
|
|
1224
|
+
except Exception:
|
|
1225
|
+
pass
|
|
1226
|
+
frame._pn_long_after = None
|
|
1061
1227
|
|
|
1062
1228
|
|
|
1063
1229
|
# ======================================================================
|
|
@@ -1074,21 +1240,11 @@ class ModalHandler(DesktopViewHandler):
|
|
|
1074
1240
|
visibility and stacking.
|
|
1075
1241
|
"""
|
|
1076
1242
|
|
|
1077
|
-
def
|
|
1078
|
-
|
|
1079
|
-
self._apply(frame, props)
|
|
1080
|
-
return frame
|
|
1081
|
-
|
|
1082
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1083
|
-
self._apply(native_view, changed)
|
|
1243
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1244
|
+
return tk.Frame(_master(), highlightthickness=0, bd=0, background="#ffffff")
|
|
1084
1245
|
|
|
1085
|
-
def
|
|
1086
|
-
merged =
|
|
1087
|
-
if merged.get("transparent"):
|
|
1088
|
-
try:
|
|
1089
|
-
frame.configure(background="#33000000".replace("33", "")) # solid fallback
|
|
1090
|
-
except Exception:
|
|
1091
|
-
pass
|
|
1246
|
+
def apply(self, frame: Any, props: Dict[str, Any]) -> None:
|
|
1247
|
+
merged = getattr(frame, "_pn_props", props)
|
|
1092
1248
|
visible = bool(merged.get("visible"))
|
|
1093
1249
|
stage = get_root_container()
|
|
1094
1250
|
try:
|
|
@@ -1099,9 +1255,16 @@ class ModalHandler(DesktopViewHandler):
|
|
|
1099
1255
|
frame.place_forget()
|
|
1100
1256
|
except Exception:
|
|
1101
1257
|
pass
|
|
1258
|
+
if visible != getattr(frame, "_pn_was_visible", None):
|
|
1259
|
+
frame._pn_was_visible = visible
|
|
1260
|
+
if visible:
|
|
1261
|
+
_fire(frame, "on_show")
|
|
1262
|
+
elif getattr(frame, "_pn_was_visible_once", False):
|
|
1263
|
+
_fire(frame, "on_dismiss")
|
|
1264
|
+
frame._pn_was_visible_once = True
|
|
1102
1265
|
|
|
1103
1266
|
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1104
|
-
# Modal placement is driven by visibility in ``
|
|
1267
|
+
# Modal placement is driven by visibility in ``apply``; the
|
|
1105
1268
|
# engine never frames the placeholder itself.
|
|
1106
1269
|
return
|
|
1107
1270
|
|
|
@@ -1114,24 +1277,19 @@ class ModalHandler(DesktopViewHandler):
|
|
|
1114
1277
|
class TabBarHandler(DesktopViewHandler):
|
|
1115
1278
|
"""Bottom tab bar — a row of buttons laid out across its width."""
|
|
1116
1279
|
|
|
1117
|
-
def
|
|
1280
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1118
1281
|
frame = tk.Frame(_master(), highlightthickness=1, bd=0, background="#f2f2f7")
|
|
1119
1282
|
try:
|
|
1120
1283
|
frame.configure(highlightbackground="#c6c6c8", highlightcolor="#c6c6c8")
|
|
1121
1284
|
except Exception:
|
|
1122
1285
|
pass
|
|
1123
1286
|
frame._pn_buttons = []
|
|
1124
|
-
self._apply(frame, props)
|
|
1125
1287
|
return frame
|
|
1126
1288
|
|
|
1127
|
-
def
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
def _apply(self, frame: Any, props: Dict[str, Any]) -> None:
|
|
1131
|
-
merged = _merge_props(frame, props)
|
|
1289
|
+
def apply(self, frame: Any, props: Dict[str, Any]) -> None:
|
|
1290
|
+
merged = getattr(frame, "_pn_props", props)
|
|
1132
1291
|
items: List[Dict[str, Any]] = merged.get("items") or []
|
|
1133
1292
|
active = merged.get("active_tab")
|
|
1134
|
-
on_select = merged.get("on_tab_select")
|
|
1135
1293
|
for button in getattr(frame, "_pn_buttons", []):
|
|
1136
1294
|
try:
|
|
1137
1295
|
button.destroy()
|
|
@@ -1144,14 +1302,7 @@ class TabBarHandler(DesktopViewHandler):
|
|
|
1144
1302
|
is_active = name == active
|
|
1145
1303
|
|
|
1146
1304
|
def _make_cmd(tab_name: Any) -> Any:
|
|
1147
|
-
|
|
1148
|
-
if callable(on_select):
|
|
1149
|
-
try:
|
|
1150
|
-
on_select(tab_name)
|
|
1151
|
-
except Exception:
|
|
1152
|
-
pass
|
|
1153
|
-
|
|
1154
|
-
return _cmd
|
|
1305
|
+
return lambda: _fire(frame, "on_tab_select", tab_name)
|
|
1155
1306
|
|
|
1156
1307
|
button = tk.Button(
|
|
1157
1308
|
frame,
|
|
@@ -1200,35 +1351,23 @@ class TabBarHandler(DesktopViewHandler):
|
|
|
1200
1351
|
|
|
1201
1352
|
|
|
1202
1353
|
class PickerHandler(DesktopViewHandler):
|
|
1203
|
-
def
|
|
1354
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1204
1355
|
combo = ttk.Combobox(_master(), state="readonly")
|
|
1205
|
-
self._bind(combo)
|
|
1206
|
-
self._apply(combo, props)
|
|
1207
|
-
return combo
|
|
1208
1356
|
|
|
1209
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1210
|
-
self._apply(native_view, changed)
|
|
1211
|
-
|
|
1212
|
-
def _bind(self, combo: Any) -> None:
|
|
1213
1357
|
def _on_select(_event: Any = None) -> None:
|
|
1214
|
-
callback = getattr(combo, "_pn_on_change", None)
|
|
1215
1358
|
items = getattr(combo, "_pn_items", [])
|
|
1216
1359
|
idx = combo.current()
|
|
1217
|
-
if
|
|
1218
|
-
|
|
1219
|
-
callback(items[idx].get("value"))
|
|
1220
|
-
except Exception:
|
|
1221
|
-
pass
|
|
1360
|
+
if 0 <= idx < len(items):
|
|
1361
|
+
_fire(combo, "on_change", items[idx].get("value"))
|
|
1222
1362
|
|
|
1223
1363
|
try:
|
|
1224
1364
|
combo.bind("<<ComboboxSelected>>", _on_select)
|
|
1225
1365
|
except Exception:
|
|
1226
1366
|
pass
|
|
1367
|
+
return combo
|
|
1227
1368
|
|
|
1228
|
-
def
|
|
1229
|
-
merged =
|
|
1230
|
-
if "on_change" in props:
|
|
1231
|
-
combo._pn_on_change = props["on_change"]
|
|
1369
|
+
def apply(self, combo: Any, props: Dict[str, Any]) -> None:
|
|
1370
|
+
merged = getattr(combo, "_pn_props", props)
|
|
1232
1371
|
items: List[Dict[str, Any]] = merged.get("items") or []
|
|
1233
1372
|
combo._pn_items = items
|
|
1234
1373
|
labels = [str(item.get("label", item.get("value", ""))) for item in items]
|
|
@@ -1251,20 +1390,15 @@ class PickerHandler(DesktopViewHandler):
|
|
|
1251
1390
|
|
|
1252
1391
|
|
|
1253
1392
|
class SegmentedControlHandler(DesktopViewHandler):
|
|
1254
|
-
def
|
|
1393
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1255
1394
|
frame = tk.Frame(_master(), highlightthickness=0, bd=0)
|
|
1256
1395
|
frame._pn_buttons = []
|
|
1257
|
-
self._apply(frame, props)
|
|
1258
1396
|
return frame
|
|
1259
1397
|
|
|
1260
|
-
def
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
def _apply(self, frame: Any, props: Dict[str, Any]) -> None:
|
|
1264
|
-
merged = _merge_props(frame, props)
|
|
1398
|
+
def apply(self, frame: Any, props: Dict[str, Any]) -> None:
|
|
1399
|
+
merged = getattr(frame, "_pn_props", props)
|
|
1265
1400
|
segments: List[str] = merged.get("segments") or []
|
|
1266
1401
|
selected = int(merged.get("selected_index", 0) or 0)
|
|
1267
|
-
on_change = merged.get("on_change")
|
|
1268
1402
|
tint = _tk_color(merged.get("tint_color")) or "#007aff"
|
|
1269
1403
|
for button in getattr(frame, "_pn_buttons", []):
|
|
1270
1404
|
try:
|
|
@@ -1276,14 +1410,7 @@ class SegmentedControlHandler(DesktopViewHandler):
|
|
|
1276
1410
|
is_active = i == selected
|
|
1277
1411
|
|
|
1278
1412
|
def _make_cmd(index: int) -> Any:
|
|
1279
|
-
|
|
1280
|
-
if callable(on_change):
|
|
1281
|
-
try:
|
|
1282
|
-
on_change(index)
|
|
1283
|
-
except Exception:
|
|
1284
|
-
pass
|
|
1285
|
-
|
|
1286
|
-
return _cmd
|
|
1413
|
+
return lambda: _fire(frame, "on_change", index)
|
|
1287
1414
|
|
|
1288
1415
|
button = tk.Button(
|
|
1289
1416
|
frame,
|
|
@@ -1329,33 +1456,20 @@ class SegmentedControlHandler(DesktopViewHandler):
|
|
|
1329
1456
|
class DatePickerHandler(DesktopViewHandler):
|
|
1330
1457
|
"""Preview DatePicker — a text entry for the ISO date/time string."""
|
|
1331
1458
|
|
|
1332
|
-
def
|
|
1459
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1333
1460
|
entry = tk.Entry(_master(), highlightthickness=1, bd=0)
|
|
1334
|
-
self._bind(entry)
|
|
1335
|
-
self._apply(entry, props)
|
|
1336
|
-
return entry
|
|
1337
|
-
|
|
1338
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1339
|
-
self._apply(native_view, changed)
|
|
1340
1461
|
|
|
1341
|
-
def _bind(self, entry: Any) -> None:
|
|
1342
1462
|
def _on_key(_event: Any = None) -> None:
|
|
1343
|
-
|
|
1344
|
-
if callable(callback):
|
|
1345
|
-
try:
|
|
1346
|
-
callback(entry.get())
|
|
1347
|
-
except Exception:
|
|
1348
|
-
pass
|
|
1463
|
+
_fire(entry, "on_change", entry.get())
|
|
1349
1464
|
|
|
1350
1465
|
try:
|
|
1351
1466
|
entry.bind("<KeyRelease>", _on_key)
|
|
1352
1467
|
except Exception:
|
|
1353
1468
|
pass
|
|
1469
|
+
return entry
|
|
1354
1470
|
|
|
1355
|
-
def
|
|
1356
|
-
merged =
|
|
1357
|
-
if "on_change" in props:
|
|
1358
|
-
entry._pn_on_change = props["on_change"]
|
|
1471
|
+
def apply(self, entry: Any, props: Dict[str, Any]) -> None:
|
|
1472
|
+
merged = getattr(entry, "_pn_props", props)
|
|
1359
1473
|
if "enabled" in merged:
|
|
1360
1474
|
try:
|
|
1361
1475
|
entry.configure(state="normal" if merged.get("enabled", True) else "disabled")
|
|
@@ -1378,78 +1492,6 @@ class DatePickerHandler(DesktopViewHandler):
|
|
|
1378
1492
|
pass
|
|
1379
1493
|
|
|
1380
1494
|
|
|
1381
|
-
# ======================================================================
|
|
1382
|
-
# VirtualList (FlatList / SectionList)
|
|
1383
|
-
# ======================================================================
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
class VirtualListHandler(DesktopViewHandler):
|
|
1387
|
-
"""Preview list — eagerly mounts a bounded window of rows.
|
|
1388
|
-
|
|
1389
|
-
The native iOS/Android backends recycle cells; the desktop preview
|
|
1390
|
-
mounts up to [`_MAX_ROWS`][pythonnative.native_views.desktop.VirtualListHandler]
|
|
1391
|
-
rows into per-row cells once its frame is known. Each cell is handed
|
|
1392
|
-
to the ``mount_row`` callback supplied by
|
|
1393
|
-
[`FlatList`][pythonnative.FlatList] / [`SectionList`][pythonnative.SectionList],
|
|
1394
|
-
which mounts the row's element subtree through a nested reconciler.
|
|
1395
|
-
"""
|
|
1396
|
-
|
|
1397
|
-
_MAX_ROWS = 200
|
|
1398
|
-
|
|
1399
|
-
def create(self, props: Dict[str, Any]) -> Any:
|
|
1400
|
-
frame = tk.Frame(_master(), highlightthickness=0, bd=0)
|
|
1401
|
-
frame._pn_rows = []
|
|
1402
|
-
frame._pn_mounted = False
|
|
1403
|
-
self._apply(frame, props)
|
|
1404
|
-
return frame
|
|
1405
|
-
|
|
1406
|
-
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1407
|
-
was_count = (getattr(native_view, "_pn_props", {}) or {}).get("count")
|
|
1408
|
-
self._apply(native_view, changed)
|
|
1409
|
-
if "count" in changed and changed.get("count") != was_count:
|
|
1410
|
-
native_view._pn_mounted = False
|
|
1411
|
-
self._mount_rows(native_view)
|
|
1412
|
-
|
|
1413
|
-
def _apply(self, frame: Any, props: Dict[str, Any]) -> Dict[str, Any]:
|
|
1414
|
-
merged = _merge_props(frame, props)
|
|
1415
|
-
_apply_common(frame, merged)
|
|
1416
|
-
return merged
|
|
1417
|
-
|
|
1418
|
-
def _mount_rows(self, frame: Any) -> None:
|
|
1419
|
-
if getattr(frame, "_pn_mounted", False):
|
|
1420
|
-
return
|
|
1421
|
-
merged = getattr(frame, "_pn_props", {}) or {}
|
|
1422
|
-
count = int(merged.get("count", 0) or 0)
|
|
1423
|
-
row_height = _finite(merged.get("row_height", 0.0))
|
|
1424
|
-
mount_row = merged.get("mount_row")
|
|
1425
|
-
frame_w, _frame_h = getattr(frame, "_pn_size", (0.0, 0.0))
|
|
1426
|
-
if count <= 0 or row_height <= 0 or not callable(mount_row) or frame_w <= 0:
|
|
1427
|
-
return
|
|
1428
|
-
for cell in getattr(frame, "_pn_rows", []):
|
|
1429
|
-
try:
|
|
1430
|
-
cell.destroy()
|
|
1431
|
-
except Exception:
|
|
1432
|
-
pass
|
|
1433
|
-
rows: List[Any] = []
|
|
1434
|
-
for index in range(min(count, self._MAX_ROWS)):
|
|
1435
|
-
cell = tk.Frame(_master(), highlightthickness=0, bd=0)
|
|
1436
|
-
cell._pn_parent = frame
|
|
1437
|
-
cell._pn_frame = (0.0, index * row_height, frame_w, row_height)
|
|
1438
|
-
_place(cell)
|
|
1439
|
-
rows.append(cell)
|
|
1440
|
-
try:
|
|
1441
|
-
mount_row(index, cell, frame_w, row_height)
|
|
1442
|
-
except Exception:
|
|
1443
|
-
pass
|
|
1444
|
-
frame._pn_rows = rows
|
|
1445
|
-
frame._pn_mounted = True
|
|
1446
|
-
|
|
1447
|
-
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1448
|
-
native_view._pn_size = (max(0.0, _finite(width)), max(0.0, _finite(height)))
|
|
1449
|
-
super().set_frame(native_view, x, y, width, height)
|
|
1450
|
-
self._mount_rows(native_view)
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
1495
|
# ======================================================================
|
|
1454
1496
|
# Registration
|
|
1455
1497
|
# ======================================================================
|
|
@@ -1459,7 +1501,9 @@ def register_handlers(registry: Any) -> None:
|
|
|
1459
1501
|
"""Register every built-in desktop handler on ``registry``.
|
|
1460
1502
|
|
|
1461
1503
|
Mirrors ``register_handlers`` in the iOS / Android backends so the
|
|
1462
|
-
desktop registry services the same
|
|
1504
|
+
desktop registry services the same element types. Lists
|
|
1505
|
+
(``FlatList`` / ``SectionList``) need no handler: they are Python
|
|
1506
|
+
components that virtualize on top of ``ScrollView``.
|
|
1463
1507
|
"""
|
|
1464
1508
|
flex = FlexContainerHandler()
|
|
1465
1509
|
registry.register("View", flex)
|
|
@@ -1482,7 +1526,6 @@ def register_handlers(registry: Any) -> None:
|
|
|
1482
1526
|
registry.register("Pressable", PressableHandler())
|
|
1483
1527
|
registry.register("StatusBar", StatusBarHandler())
|
|
1484
1528
|
registry.register("KeyboardAvoidingView", KeyboardAvoidingViewHandler())
|
|
1485
|
-
registry.register("VirtualList", VirtualListHandler())
|
|
1486
1529
|
registry.register("Picker", PickerHandler())
|
|
1487
1530
|
registry.register("Checkbox", CheckboxHandler())
|
|
1488
1531
|
registry.register("SegmentedControl", SegmentedControlHandler())
|