pythonnative 0.22.1__py3-none-any.whl → 0.23.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.
Files changed (25) hide show
  1. pythonnative/__init__.py +16 -2
  2. pythonnative/appearance.py +124 -0
  3. pythonnative/components.py +708 -38
  4. pythonnative/hooks.py +41 -0
  5. pythonnative/images.py +196 -0
  6. pythonnative/mutations.py +3 -12
  7. pythonnative/native_views/__init__.py +1 -7
  8. pythonnative/native_views/android.py +573 -45
  9. pythonnative/native_views/desktop.py +78 -19
  10. pythonnative/native_views/ios.py +739 -51
  11. pythonnative/preview.py +31 -0
  12. pythonnative/project/config.py +1 -9
  13. pythonnative/screen.py +85 -0
  14. pythonnative/style.py +65 -6
  15. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
  16. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
  17. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
  18. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
  19. pythonnative/virtual_rows.py +137 -0
  20. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/METADATA +1 -1
  21. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/RECORD +25 -19
  22. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/WHEEL +1 -1
  23. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/entry_points.txt +0 -0
  24. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/licenses/LICENSE +0 -0
  25. {pythonnative-0.22.1.dist-info → pythonnative-0.23.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
- if any(k in props for k in ("border_width", "border_color")):
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)
@@ -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; network URLs
945
- and JPEG aren't supported without Pillow, so those fall back to a
946
- labeled placeholder. The handler keeps a reference to the
947
- ``PhotoImage`` (Tk garbage-collects images that aren't referenced).
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 "source" in props:
956
- source = props.get("source")
957
- photo = None
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
- photo = tk.PhotoImage(file=str(source))
983
+ label.configure(background=color)
961
984
  except Exception:
962
- photo = None
963
- label._pn_photo = photo # keep a reference alive
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
- if photo is not None:
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
- pass
972
- _apply_common(label, merged)
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)