pythonnative 0.22.1__py3-none-any.whl → 0.24.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 +43 -50
- pythonnative/animated.py +1 -1
- pythonnative/appearance.py +124 -0
- pythonnative/cli/pn.py +3 -3
- pythonnative/components.py +873 -466
- pythonnative/diagnostics.py +214 -0
- pythonnative/element.py +5 -2
- pythonnative/events.py +13 -8
- pythonnative/hooks.py +454 -49
- pythonnative/hot_reload.py +9 -1
- pythonnative/images.py +196 -0
- pythonnative/mutations.py +3 -12
- pythonnative/native_views/__init__.py +1 -7
- pythonnative/native_views/android.py +651 -46
- pythonnative/native_views/desktop.py +116 -20
- pythonnative/native_views/ios.py +974 -52
- pythonnative/navigation.py +41 -17
- pythonnative/preview.py +74 -7
- pythonnative/project/config.py +1 -9
- pythonnative/reconciler.py +863 -441
- pythonnative/screen.py +409 -27
- pythonnative/storage.py +3 -3
- pythonnative/style.py +148 -6
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
- pythonnative/virtual_rows.py +137 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/METADATA +5 -4
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/RECORD +35 -28
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/top_level.txt +0 -0
|
@@ -376,10 +376,27 @@ def _apply_common(widget: Any, props: Dict[str, Any]) -> None:
|
|
|
376
376
|
widget.configure(background=color)
|
|
377
377
|
except Exception:
|
|
378
378
|
pass
|
|
379
|
-
|
|
379
|
+
side_width_keys = (
|
|
380
|
+
"border_left_width",
|
|
381
|
+
"border_top_width",
|
|
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)):
|
|
380
386
|
try:
|
|
381
387
|
width = props.get("border_width")
|
|
382
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
|
|
383
400
|
if width:
|
|
384
401
|
widget.configure(
|
|
385
402
|
highlightthickness=int(round(_finite(width))),
|
|
@@ -390,6 +407,10 @@ def _apply_common(widget: Any, props: Dict[str, Any]) -> None:
|
|
|
390
407
|
widget.configure(highlightthickness=0)
|
|
391
408
|
except Exception:
|
|
392
409
|
pass
|
|
410
|
+
if "test_id" in props and props["test_id"] is not None:
|
|
411
|
+
# Stamped for introspection so preview-level tooling and tests
|
|
412
|
+
# can locate widgets the same way Maestro does on device.
|
|
413
|
+
widget._pn_test_id = str(props["test_id"])
|
|
393
414
|
if "transform" in props:
|
|
394
415
|
_set_translate_from_transform(widget, props["transform"])
|
|
395
416
|
_place(widget)
|
|
@@ -782,7 +803,7 @@ class TextHandler(DesktopViewHandler):
|
|
|
782
803
|
class ButtonHandler(DesktopViewHandler):
|
|
783
804
|
def build(self, props: Dict[str, Any]) -> Any:
|
|
784
805
|
button = tk.Button(_master(), highlightthickness=0, takefocus=0)
|
|
785
|
-
button.configure(command=lambda: _fire(button, "
|
|
806
|
+
button.configure(command=lambda: _fire(button, "on_press"))
|
|
786
807
|
return button
|
|
787
808
|
|
|
788
809
|
def apply(self, button: Any, props: Dict[str, Any]) -> None:
|
|
@@ -941,10 +962,13 @@ class TextInputHandler(DesktopViewHandler):
|
|
|
941
962
|
class ImageHandler(DesktopViewHandler):
|
|
942
963
|
"""Best-effort image preview.
|
|
943
964
|
|
|
944
|
-
Tk's ``PhotoImage`` loads PNG/GIF/PPM from local paths;
|
|
945
|
-
|
|
946
|
-
labeled placeholder.
|
|
947
|
-
|
|
965
|
+
Tk's ``PhotoImage`` loads PNG/GIF/PPM from local paths; JPEG isn't
|
|
966
|
+
supported without Pillow, so undecodable formats fall back to a
|
|
967
|
+
labeled placeholder. Network URLs are fetched through the shared
|
|
968
|
+
image pipeline (`pythonnative.images`), so caching and ``on_load``
|
|
969
|
+
/ ``on_error`` behave like the mobile backends. The handler keeps
|
|
970
|
+
a reference to the ``PhotoImage`` (Tk garbage-collects images that
|
|
971
|
+
aren't referenced).
|
|
948
972
|
"""
|
|
949
973
|
|
|
950
974
|
def build(self, props: Dict[str, Any]) -> Any:
|
|
@@ -952,24 +976,59 @@ class ImageHandler(DesktopViewHandler):
|
|
|
952
976
|
|
|
953
977
|
def apply(self, label: Any, props: Dict[str, Any]) -> None:
|
|
954
978
|
merged = getattr(label, "_pn_props", props)
|
|
955
|
-
if "
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
if source and "://" not in str(source):
|
|
979
|
+
if "placeholder_color" in props:
|
|
980
|
+
color = _tk_color(props.get("placeholder_color"))
|
|
981
|
+
if color is not None:
|
|
959
982
|
try:
|
|
960
|
-
|
|
983
|
+
label.configure(background=color)
|
|
961
984
|
except Exception:
|
|
962
|
-
|
|
963
|
-
|
|
985
|
+
pass
|
|
986
|
+
if "source" in props:
|
|
987
|
+
source = props.get("source")
|
|
988
|
+
label._pn_pending_source = source
|
|
989
|
+
if source and "://" in str(source):
|
|
990
|
+
from ..images import fetch
|
|
991
|
+
|
|
992
|
+
def _on_ready(path: str, src: Any = source) -> None:
|
|
993
|
+
if getattr(label, "_pn_pending_source", None) == src:
|
|
994
|
+
self._show_file(label, path, src)
|
|
995
|
+
|
|
996
|
+
def _on_error(message: str, src: Any = source) -> None:
|
|
997
|
+
if getattr(label, "_pn_pending_source", None) == src:
|
|
998
|
+
self._show_fallback(label, src)
|
|
999
|
+
_fire(label, "on_error", message)
|
|
1000
|
+
|
|
1001
|
+
self._show_fallback(label, source)
|
|
1002
|
+
fetch(str(source), _on_ready, _on_error)
|
|
1003
|
+
else:
|
|
1004
|
+
self._show_file(label, str(source) if source else None, source)
|
|
1005
|
+
_apply_common(label, merged)
|
|
1006
|
+
|
|
1007
|
+
def _show_file(self, label: Any, path: Optional[str], source: Any) -> None:
|
|
1008
|
+
photo = None
|
|
1009
|
+
if path:
|
|
964
1010
|
try:
|
|
965
|
-
|
|
966
|
-
label.configure(image=photo, text="")
|
|
967
|
-
else:
|
|
968
|
-
name = str(source).rsplit("/", 1)[-1] if source else "image"
|
|
969
|
-
label.configure(image="", text=f"\U0001f5bc\n{name}", compound="center")
|
|
1011
|
+
photo = tk.PhotoImage(file=path)
|
|
970
1012
|
except Exception:
|
|
971
|
-
|
|
972
|
-
|
|
1013
|
+
photo = None
|
|
1014
|
+
label._pn_photo = photo # keep a reference alive
|
|
1015
|
+
try:
|
|
1016
|
+
if photo is not None:
|
|
1017
|
+
label.configure(image=photo, text="")
|
|
1018
|
+
_fire(label, "on_load")
|
|
1019
|
+
else:
|
|
1020
|
+
self._show_fallback(label, source)
|
|
1021
|
+
if path:
|
|
1022
|
+
_fire(label, "on_error", "decode failed")
|
|
1023
|
+
except Exception:
|
|
1024
|
+
pass
|
|
1025
|
+
|
|
1026
|
+
def _show_fallback(self, label: Any, source: Any) -> None:
|
|
1027
|
+
name = str(source).rsplit("/", 1)[-1] if source else "image"
|
|
1028
|
+
try:
|
|
1029
|
+
label.configure(image="", text=f"\U0001f5bc\n{name}", compound="center")
|
|
1030
|
+
except Exception:
|
|
1031
|
+
pass
|
|
973
1032
|
|
|
974
1033
|
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
975
1034
|
photo = getattr(native_view, "_pn_photo", None)
|
|
@@ -1269,6 +1328,42 @@ class ModalHandler(DesktopViewHandler):
|
|
|
1269
1328
|
return
|
|
1270
1329
|
|
|
1271
1330
|
|
|
1331
|
+
# ======================================================================
|
|
1332
|
+
# Portal
|
|
1333
|
+
# ======================================================================
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
class PortalHandler(DesktopViewHandler):
|
|
1337
|
+
"""Portal host that floats children over the whole stage.
|
|
1338
|
+
|
|
1339
|
+
Tkinter has no view that can cover the stage without also
|
|
1340
|
+
swallowing clicks, so the portal keeps its own frame unmapped and
|
|
1341
|
+
instead places each child directly against the stage (``_place``
|
|
1342
|
+
with no logical parent targets the root container). The detached
|
|
1343
|
+
layout pass produces viewport coordinates, which is exactly the
|
|
1344
|
+
stage coordinate space, and ``_place`` lifts children above the
|
|
1345
|
+
main content. Empty portal area has no widget at all, so clicks
|
|
1346
|
+
there reach whatever sits underneath.
|
|
1347
|
+
"""
|
|
1348
|
+
|
|
1349
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
1350
|
+
# No ``_pn_parent``: placement composes against the stage.
|
|
1351
|
+
child._pn_parent = None
|
|
1352
|
+
_register_child(parent, child, index)
|
|
1353
|
+
_place(child)
|
|
1354
|
+
|
|
1355
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
1356
|
+
_unregister_child(parent, child)
|
|
1357
|
+
try:
|
|
1358
|
+
child.place_forget()
|
|
1359
|
+
except Exception:
|
|
1360
|
+
pass
|
|
1361
|
+
|
|
1362
|
+
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1363
|
+
# The portal frame itself stays unmapped; only children render.
|
|
1364
|
+
return
|
|
1365
|
+
|
|
1366
|
+
|
|
1272
1367
|
# ======================================================================
|
|
1273
1368
|
# TabBar
|
|
1274
1369
|
# ======================================================================
|
|
@@ -1521,6 +1616,7 @@ def register_handlers(registry: Any) -> None:
|
|
|
1521
1616
|
registry.register("ScrollView", ScrollViewHandler())
|
|
1522
1617
|
registry.register("SafeAreaView", SafeAreaViewHandler())
|
|
1523
1618
|
registry.register("Modal", ModalHandler())
|
|
1619
|
+
registry.register("Portal", PortalHandler())
|
|
1524
1620
|
registry.register("Slider", SliderHandler())
|
|
1525
1621
|
registry.register("TabBar", TabBarHandler())
|
|
1526
1622
|
registry.register("Pressable", PressableHandler())
|