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
pythonnative/navigation.py
CHANGED
|
@@ -45,11 +45,12 @@ Example:
|
|
|
45
45
|
```
|
|
46
46
|
"""
|
|
47
47
|
|
|
48
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
48
|
+
from typing import Any, Callable, Dict, List, Optional, TypedDict, Union
|
|
49
49
|
|
|
50
50
|
from .element import Element
|
|
51
51
|
from .hooks import (
|
|
52
52
|
Provider,
|
|
53
|
+
Ref,
|
|
53
54
|
_NavigationContext,
|
|
54
55
|
component,
|
|
55
56
|
create_context,
|
|
@@ -60,6 +61,29 @@ from .hooks import (
|
|
|
60
61
|
use_state,
|
|
61
62
|
)
|
|
62
63
|
|
|
64
|
+
|
|
65
|
+
class ScreenOptions(TypedDict, total=False):
|
|
66
|
+
"""Typed per-screen options accepted by ``Screen(..., options=...)``.
|
|
67
|
+
|
|
68
|
+
All keys are optional. Navigators ignore keys they don't use.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
title: Human-readable screen title. Stack navigators forward it
|
|
72
|
+
to the host's native navigation bar; tab and drawer
|
|
73
|
+
navigators use it as the item label.
|
|
74
|
+
tab_bar_icon: Native system icon identifier for tab items. A
|
|
75
|
+
string is used on every platform; a dict like
|
|
76
|
+
``{"ios": "house.fill", "android": "ic_menu_home"}``
|
|
77
|
+
selects per platform. iOS values are resolved via
|
|
78
|
+
SF Symbols (``UIImage.systemImageNamed_``); Android values
|
|
79
|
+
are resolved against ``android.R.drawable.<name>``. Names
|
|
80
|
+
that don't resolve fall back to text-only.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
title: str
|
|
84
|
+
tab_bar_icon: Union[str, Dict[str, str]]
|
|
85
|
+
|
|
86
|
+
|
|
63
87
|
# ======================================================================
|
|
64
88
|
# Focus context
|
|
65
89
|
# ======================================================================
|
|
@@ -85,14 +109,14 @@ class _ScreenDef:
|
|
|
85
109
|
name: Route name used by `navigate(name, ...)`.
|
|
86
110
|
component: A `@component` function rendered when this screen is
|
|
87
111
|
active.
|
|
88
|
-
options:
|
|
89
|
-
|
|
90
|
-
specific navigator implementation.
|
|
112
|
+
options: Per-screen [`ScreenOptions`][pythonnative.ScreenOptions]
|
|
113
|
+
(e.g., `title`, `tab_bar_icon`). Interpretation is up to
|
|
114
|
+
the specific navigator implementation.
|
|
91
115
|
"""
|
|
92
116
|
|
|
93
117
|
__slots__ = ("name", "component", "options")
|
|
94
118
|
|
|
95
|
-
def __init__(self, name: str, component_fn: Any, options: Optional[
|
|
119
|
+
def __init__(self, name: str, component_fn: Any, options: Optional[ScreenOptions] = None) -> None:
|
|
96
120
|
self.name = name
|
|
97
121
|
self.component = component_fn
|
|
98
122
|
self.options = options or {}
|
|
@@ -450,10 +474,10 @@ def _stack_navigator_impl(screens: Any = None, initial_route: Optional[str] = No
|
|
|
450
474
|
stack, set_stack = use_state(lambda: [_RouteEntry(first_route, first_params)])
|
|
451
475
|
|
|
452
476
|
stack_ref = use_ref(None)
|
|
453
|
-
stack_ref
|
|
477
|
+
stack_ref.current = stack
|
|
454
478
|
|
|
455
479
|
handle = use_memo(
|
|
456
|
-
lambda: _DeclarativeNavHandle(screen_map, lambda: stack_ref
|
|
480
|
+
lambda: _DeclarativeNavHandle(screen_map, lambda: stack_ref.current, set_stack, parent=parent_nav), []
|
|
457
481
|
)
|
|
458
482
|
handle._screen_map = screen_map
|
|
459
483
|
handle._parent = parent_nav
|
|
@@ -505,7 +529,7 @@ def create_stack_navigator() -> Any:
|
|
|
505
529
|
|
|
506
530
|
class _StackNavigator:
|
|
507
531
|
@staticmethod
|
|
508
|
-
def Screen(name: str, *, component: Any, options: Optional[
|
|
532
|
+
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
509
533
|
"""Define a screen within this stack navigator.
|
|
510
534
|
|
|
511
535
|
Args:
|
|
@@ -556,8 +580,8 @@ def _tab_navigator_impl(screens: Any = None, initial_route: Optional[str] = None
|
|
|
556
580
|
active_tab, set_active_tab = use_state(first_route)
|
|
557
581
|
tab_params, set_tab_params = use_state(lambda: {first_route: {}})
|
|
558
582
|
|
|
559
|
-
params_ref = use_ref(None)
|
|
560
|
-
params_ref
|
|
583
|
+
params_ref: Ref[Dict[str, Dict[str, Any]]] = use_ref(None)
|
|
584
|
+
params_ref.current = tab_params
|
|
561
585
|
|
|
562
586
|
def switch_tab(name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
563
587
|
set_active_tab(name)
|
|
@@ -565,7 +589,7 @@ def _tab_navigator_impl(screens: Any = None, initial_route: Optional[str] = None
|
|
|
565
589
|
set_tab_params(lambda p: {**p, name: params})
|
|
566
590
|
|
|
567
591
|
def get_stack() -> List[_RouteEntry]:
|
|
568
|
-
p = params_ref
|
|
592
|
+
p = params_ref.current or {}
|
|
569
593
|
return [_RouteEntry(active_tab, p.get(active_tab, {}))]
|
|
570
594
|
|
|
571
595
|
handle = use_memo(lambda: _TabNavHandle(screen_map, get_stack, lambda _: None, switch_tab, parent=parent_nav), [])
|
|
@@ -643,7 +667,7 @@ def create_tab_navigator() -> Any:
|
|
|
643
667
|
|
|
644
668
|
class _TabNavigator:
|
|
645
669
|
@staticmethod
|
|
646
|
-
def Screen(name: str, *, component: Any, options: Optional[
|
|
670
|
+
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
647
671
|
"""Define a screen within this tab navigator.
|
|
648
672
|
|
|
649
673
|
Args:
|
|
@@ -704,8 +728,8 @@ def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = N
|
|
|
704
728
|
drawer_open, set_drawer_open = use_state(False)
|
|
705
729
|
screen_params, set_screen_params = use_state(lambda: {first_route: {}})
|
|
706
730
|
|
|
707
|
-
params_ref = use_ref(None)
|
|
708
|
-
params_ref
|
|
731
|
+
params_ref: Ref[Dict[str, Dict[str, Any]]] = use_ref(None)
|
|
732
|
+
params_ref.current = screen_params
|
|
709
733
|
|
|
710
734
|
def switch_screen(name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
711
735
|
set_active_screen(name)
|
|
@@ -713,7 +737,7 @@ def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = N
|
|
|
713
737
|
set_screen_params(lambda p: {**p, name: params})
|
|
714
738
|
|
|
715
739
|
def get_stack() -> List[_RouteEntry]:
|
|
716
|
-
p = params_ref
|
|
740
|
+
p = params_ref.current or {}
|
|
717
741
|
return [_RouteEntry(active_screen, p.get(active_screen, {}))]
|
|
718
742
|
|
|
719
743
|
handle = use_memo(
|
|
@@ -763,7 +787,7 @@ def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = N
|
|
|
763
787
|
return _select
|
|
764
788
|
|
|
765
789
|
menu_items.append(
|
|
766
|
-
Element("Button", {"title": label, "
|
|
790
|
+
Element("Button", {"title": label, "on_press": make_select(item_name)}, [], key=f"__drawer_{item_name}")
|
|
767
791
|
)
|
|
768
792
|
|
|
769
793
|
drawer_panel = Element(
|
|
@@ -813,7 +837,7 @@ def create_drawer_navigator() -> Any:
|
|
|
813
837
|
|
|
814
838
|
class _DrawerNavigator:
|
|
815
839
|
@staticmethod
|
|
816
|
-
def Screen(name: str, *, component: Any, options: Optional[
|
|
840
|
+
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
817
841
|
"""Define a screen within this drawer navigator.
|
|
818
842
|
|
|
819
843
|
Args:
|
pythonnative/preview.py
CHANGED
|
@@ -43,6 +43,36 @@ _POLL_INTERVAL_MS = 16
|
|
|
43
43
|
_WATCH_INTERVAL_S = 0.4
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
def _publish_desktop_color_scheme() -> None:
|
|
47
|
+
"""Publish the host OS appearance so `use_color_scheme` works in preview.
|
|
48
|
+
|
|
49
|
+
``PN_COLOR_SCHEME=light|dark`` forces a value (handy for testing
|
|
50
|
+
both appearances); otherwise macOS is asked via ``defaults read``
|
|
51
|
+
(the key only exists when dark mode is on). Other platforms default
|
|
52
|
+
to light.
|
|
53
|
+
"""
|
|
54
|
+
from . import appearance
|
|
55
|
+
|
|
56
|
+
forced = os.environ.get("PN_COLOR_SCHEME")
|
|
57
|
+
if forced in ("light", "dark"):
|
|
58
|
+
appearance.set_system_color_scheme(forced)
|
|
59
|
+
return
|
|
60
|
+
if sys.platform == "darwin":
|
|
61
|
+
import subprocess
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
["defaults", "read", "-g", "AppleInterfaceStyle"],
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
timeout=2,
|
|
69
|
+
)
|
|
70
|
+
is_dark = result.returncode == 0 and result.stdout.strip() == "Dark"
|
|
71
|
+
appearance.set_system_color_scheme("dark" if is_dark else "light")
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
46
76
|
class DesktopApp:
|
|
47
77
|
"""Navigation-stack controller for the desktop preview window.
|
|
48
78
|
|
|
@@ -201,17 +231,13 @@ class DesktopApp:
|
|
|
201
231
|
host.on_pause()
|
|
202
232
|
except Exception:
|
|
203
233
|
pass
|
|
234
|
+
# ``on_destroy`` unmounts the host's reconciler: effect cleanups
|
|
235
|
+
# run, native widgets are destroyed, and event registrations are
|
|
236
|
+
# released (see ``screen._destroy_host``).
|
|
204
237
|
try:
|
|
205
238
|
host.on_destroy()
|
|
206
239
|
except Exception:
|
|
207
240
|
pass
|
|
208
|
-
reconciler = getattr(host, "_reconciler", None)
|
|
209
|
-
tree = getattr(reconciler, "_tree", None) if reconciler is not None else None
|
|
210
|
-
if reconciler is not None and tree is not None:
|
|
211
|
-
try:
|
|
212
|
-
reconciler._destroy_tree(tree)
|
|
213
|
-
except Exception:
|
|
214
|
-
pass
|
|
215
241
|
container = getattr(host, "_pn_container", None)
|
|
216
242
|
if container is not None:
|
|
217
243
|
try:
|
|
@@ -219,6 +245,29 @@ class DesktopApp:
|
|
|
219
245
|
except Exception:
|
|
220
246
|
pass
|
|
221
247
|
|
|
248
|
+
def teardown_all(self) -> None:
|
|
249
|
+
"""Destroy every screen on the stack (preview window closing)."""
|
|
250
|
+
while self._stack:
|
|
251
|
+
self._teardown(self._stack.pop())
|
|
252
|
+
|
|
253
|
+
def back_pressed(self) -> None:
|
|
254
|
+
"""Route a desktop back gesture (Escape) like a hardware back press.
|
|
255
|
+
|
|
256
|
+
``use_back_handler`` subscribers on the active screen get the
|
|
257
|
+
first chance to consume the event; otherwise the stack pops
|
|
258
|
+
(matching Android's default back behavior). At the root the
|
|
259
|
+
event is ignored.
|
|
260
|
+
"""
|
|
261
|
+
host = self.active_host()
|
|
262
|
+
if host is None:
|
|
263
|
+
return
|
|
264
|
+
try:
|
|
265
|
+
if host.on_back_pressed():
|
|
266
|
+
return
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
self.pop_screen()
|
|
270
|
+
|
|
222
271
|
# -- viewport / resize --------------------------------------------
|
|
223
272
|
|
|
224
273
|
def resize(self, width: float, height: float) -> None:
|
|
@@ -359,6 +408,13 @@ def run_preview(
|
|
|
359
408
|
)
|
|
360
409
|
|
|
361
410
|
root_dir, watched = _resolve_paths(component_path, project_root, watch_dir)
|
|
411
|
+
_publish_desktop_color_scheme()
|
|
412
|
+
|
|
413
|
+
# The preview is inherently a development surface: turn on dev
|
|
414
|
+
# diagnostics (validation warnings, hook-order checks, RedBox).
|
|
415
|
+
from . import diagnostics
|
|
416
|
+
|
|
417
|
+
diagnostics.set_dev_mode(True)
|
|
362
418
|
|
|
363
419
|
root = tk.Tk()
|
|
364
420
|
root.title(title)
|
|
@@ -383,6 +439,11 @@ def run_preview(
|
|
|
383
439
|
|
|
384
440
|
stage.bind("<Configure>", _on_configure)
|
|
385
441
|
|
|
442
|
+
# Escape acts as the desktop stand-in for the hardware back button:
|
|
443
|
+
# ``use_back_handler`` subscribers can intercept it, and otherwise
|
|
444
|
+
# the navigation stack pops.
|
|
445
|
+
root.bind("<Escape>", lambda _event: app.back_pressed())
|
|
446
|
+
|
|
386
447
|
watcher = _build_watcher(watched, root_dir, app, main_queue) if hot_reload else None
|
|
387
448
|
if watcher is not None:
|
|
388
449
|
watcher.start()
|
|
@@ -415,6 +476,12 @@ def run_preview(
|
|
|
415
476
|
watcher.stop()
|
|
416
477
|
except Exception:
|
|
417
478
|
pass
|
|
479
|
+
# Unmount every screen first so effect cleanups (timers, tasks,
|
|
480
|
+
# subscriptions) run before the Tk interpreter goes away.
|
|
481
|
+
try:
|
|
482
|
+
app.teardown_all()
|
|
483
|
+
except Exception:
|
|
484
|
+
pass
|
|
418
485
|
runtime_module.set_desktop_main_dispatch(None)
|
|
419
486
|
desktop_backend.clear_root_container()
|
|
420
487
|
try:
|
pythonnative/project/config.py
CHANGED
|
@@ -321,15 +321,7 @@ class AppConfig:
|
|
|
321
321
|
root = Path(project_root) if project_root is not None else Path.cwd()
|
|
322
322
|
config_path = root / CONFIG_FILENAME
|
|
323
323
|
if not config_path.is_file():
|
|
324
|
-
|
|
325
|
-
hint = ""
|
|
326
|
-
if legacy.is_file():
|
|
327
|
-
hint = (
|
|
328
|
-
"\nFound a legacy 'pythonnative.json'. PythonNative now uses "
|
|
329
|
-
"'pythonnative.toml'.\nRun 'pn init --force' to scaffold one, then "
|
|
330
|
-
"port your settings over."
|
|
331
|
-
)
|
|
332
|
-
raise ConfigError(f"No {CONFIG_FILENAME} found in {root}.{hint}")
|
|
324
|
+
raise ConfigError(f"No {CONFIG_FILENAME} found in {root}.")
|
|
333
325
|
try:
|
|
334
326
|
with open(config_path, "rb") as handle:
|
|
335
327
|
data = _toml.load(handle)
|