nextpytk 0.4.6__tar.gz → 0.4.8__tar.gz
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.
- {nextpytk-0.4.6 → nextpytk-0.4.8}/PKG-INFO +11 -1
- {nextpytk-0.4.6 → nextpytk-0.4.8}/README.md +10 -0
- nextpytk-0.4.8/ROADMAP.md +61 -0
- {nextpytk-0.4.6 → nextpytk-0.4.8}/pyproject.toml +1 -1
- {nextpytk-0.4.6 → nextpytk-0.4.8}/src/nextpytk/app.py +112 -19
- {nextpytk-0.4.6 → nextpytk-0.4.8}/src/nextpytk/layout.py +56 -14
- {nextpytk-0.4.6 → nextpytk-0.4.8}/src/nextpytk/theme.py +56 -26
- {nextpytk-0.4.6 → nextpytk-0.4.8}/src/nextpytk/tokens.py +1 -1
- nextpytk-0.4.6/ROADMAP.md +0 -377
- {nextpytk-0.4.6 → nextpytk-0.4.8}/.gitignore +0 -0
- {nextpytk-0.4.6 → nextpytk-0.4.8}/LICENSE +0 -0
- {nextpytk-0.4.6 → nextpytk-0.4.8}/src/nextpytk/__init__.py +0 -0
- {nextpytk-0.4.6 → nextpytk-0.4.8}/src/nextpytk/types.py +0 -0
- {nextpytk-0.4.6 → nextpytk-0.4.8}/src/nextpytk/widgets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nextpytk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary: Accessible, declarative Tkinter applications from ordinary Python functions.
|
|
5
5
|
Project-URL: Homepage, https://github.com/nishimotz/nextpytk
|
|
6
6
|
Project-URL: Repository, https://github.com/nishimotz/nextpytk
|
|
@@ -495,6 +495,11 @@ panes and both gutters share a **single vertical scrollbar**, so the gutters
|
|
|
495
495
|
stay in lock-step with the content (no drift). Logical line numbers
|
|
496
496
|
(`1..n`) follow the pane content; the right pane's numbers update as you edit.
|
|
497
497
|
|
|
498
|
+
> **Note:** `line_numbers=True` takes precedence over `sync_yscroll`. The
|
|
499
|
+
> shared-scrollbar layout it installs keeps both panes (and both gutters)
|
|
500
|
+
> scrolled in lock-step, so passing `sync_yscroll=False` together with
|
|
501
|
+
> `line_numbers=True` has no effect. If the panes must scroll independently,
|
|
502
|
+
> leave `line_numbers` off.
|
|
498
503
|
|
|
499
504
|
### Wrap layout
|
|
500
505
|
|
|
@@ -853,6 +858,7 @@ uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async
|
|
|
853
858
|
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
854
859
|
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
855
860
|
uv run python examples/bottom_bar_demo.py # pinned bottom bar (section side="bottom")
|
|
861
|
+
uv run python examples/live_validation.py # live validation via Tcl-var trace ingest (ingest_trace=True)
|
|
856
862
|
```
|
|
857
863
|
|
|
858
864
|
---
|
|
@@ -875,6 +881,10 @@ uv run python examples/bottom_bar_demo.py # pinned bottom bar (section sid
|
|
|
875
881
|
- **`CustomTkinter`**: Modern look via Canvas rendering. nextpytk takes the opposite approach: use native widgets and embed A11y from the start.
|
|
876
882
|
- **`TkRouter`** (israel-dryer, author of ttkbootstrap): Declarative view routing with URL-style paths, animated transitions, and history stack. Complements nextpytk's `multiview` — routing vs widget composition.
|
|
877
883
|
|
|
884
|
+
## Changelog
|
|
885
|
+
|
|
886
|
+
See [CHANGES.md](./CHANGES.md) for the full release history.
|
|
887
|
+
|
|
878
888
|
## License
|
|
879
889
|
|
|
880
890
|
MIT
|
|
@@ -471,6 +471,11 @@ panes and both gutters share a **single vertical scrollbar**, so the gutters
|
|
|
471
471
|
stay in lock-step with the content (no drift). Logical line numbers
|
|
472
472
|
(`1..n`) follow the pane content; the right pane's numbers update as you edit.
|
|
473
473
|
|
|
474
|
+
> **Note:** `line_numbers=True` takes precedence over `sync_yscroll`. The
|
|
475
|
+
> shared-scrollbar layout it installs keeps both panes (and both gutters)
|
|
476
|
+
> scrolled in lock-step, so passing `sync_yscroll=False` together with
|
|
477
|
+
> `line_numbers=True` has no effect. If the panes must scroll independently,
|
|
478
|
+
> leave `line_numbers` off.
|
|
474
479
|
|
|
475
480
|
### Wrap layout
|
|
476
481
|
|
|
@@ -829,6 +834,7 @@ uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async
|
|
|
829
834
|
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
830
835
|
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
831
836
|
uv run python examples/bottom_bar_demo.py # pinned bottom bar (section side="bottom")
|
|
837
|
+
uv run python examples/live_validation.py # live validation via Tcl-var trace ingest (ingest_trace=True)
|
|
832
838
|
```
|
|
833
839
|
|
|
834
840
|
---
|
|
@@ -851,6 +857,10 @@ uv run python examples/bottom_bar_demo.py # pinned bottom bar (section sid
|
|
|
851
857
|
- **`CustomTkinter`**: Modern look via Canvas rendering. nextpytk takes the opposite approach: use native widgets and embed A11y from the start.
|
|
852
858
|
- **`TkRouter`** (israel-dryer, author of ttkbootstrap): Declarative view routing with URL-style paths, animated transitions, and history stack. Complements nextpytk's `multiview` — routing vs widget composition.
|
|
853
859
|
|
|
860
|
+
## Changelog
|
|
861
|
+
|
|
862
|
+
See [CHANGES.md](./CHANGES.md) for the full release history.
|
|
863
|
+
|
|
854
864
|
## License
|
|
855
865
|
|
|
856
866
|
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# ROADMAP — nextpytk
|
|
2
|
+
|
|
3
|
+
> **Note:** Past release history lives in [CHANGES.md](./CHANGES.md).
|
|
4
|
+
> This file is the forward-looking plan (near-term, then v0.5.0, then longer term).
|
|
5
|
+
|
|
6
|
+
## Near term (post-0.4.8)
|
|
7
|
+
|
|
8
|
+
### A11y
|
|
9
|
+
|
|
10
|
+
- [ ] `@app.status` live-region behavior (ARIA `aria-live` equivalent / WCAG 4.1.3)
|
|
11
|
+
- [ ] Tk 9.1 `tk accessible set_acc_*` real-device verification (9.1b0 + NVDA)
|
|
12
|
+
- [ ] Role vocabulary mapping to Tk side
|
|
13
|
+
- [ ] Automatic `emit_selection_change` (leverage `apply_state` knowing which widgets changed)
|
|
14
|
+
|
|
15
|
+
### Type hints
|
|
16
|
+
|
|
17
|
+
- [ ] Further TypedDict / Protocol coverage for decorator arguments
|
|
18
|
+
- [ ] Fluent API equivalent to `_GridBuilder.columnconfigure`
|
|
19
|
+
|
|
20
|
+
### Widget expansion
|
|
21
|
+
|
|
22
|
+
- [ ] Per-widget `bind` for other widgets (`text`, `combobox`, `treeview`, `button`, etc.)
|
|
23
|
+
- [ ] `@app.filepicker` — `filedialog` wrapper (schema/tool naming still open)
|
|
24
|
+
|
|
25
|
+
### Layout DSL
|
|
26
|
+
|
|
27
|
+
- [ ] CSS-grid-inspired layout API for 0.5.0: explicit `grid-template-columns` / `grid-template-rows` strings (e.g. `"1fr 2fr"`, `"auto 1fr"`), area-based placement, and gap tokens. Reduce misuse by making rows/columns and widget placement visually aligned in one declaration.
|
|
28
|
+
- [ ] Wire visual Tab order for `side="bottom"` sections via `_wire_tab_order` (see v0.4.6 note in CHANGES.md).
|
|
29
|
+
|
|
30
|
+
### Agent / LLM integration
|
|
31
|
+
|
|
32
|
+
- [ ] Expose `schema()` as Function Calling definition (bind `sequence`, treeview column defs, etc.)
|
|
33
|
+
- [ ] `@agent_tool` integration (GUI operations as agent vocabulary)
|
|
34
|
+
- [ ] Surface `@app.filepicker` in `schema()` / tool vocabulary
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## v0.5.0 — themes / chrome decoupling
|
|
39
|
+
|
|
40
|
+
- Remove deprecated `theme=True/False` support; `theme` becomes `str` only.
|
|
41
|
+
- Remove deprecated `EventSeq.DOUBLE_CLICK`; use `EventSeq.PRIMARY_DOUBLE_CLICK` or `DOUBLE_BUTTON_1`.
|
|
42
|
+
- Remove deprecated index-order plural methods `col_weights`/`row_weights`/`col_minsizes`/`row_minsizes` and `Layout.cluster()` alias.
|
|
43
|
+
- Decouple `Layout().header()` / `.status()` chrome from Kizashi-specific tokens so they render safely with any ttk theme (`"none"`, `"clam"`, etc.).
|
|
44
|
+
- Extract Kizashi as a standalone ttk theme package so it can be proposed upstream or reused independently of nextpytk.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Longer term
|
|
49
|
+
|
|
50
|
+
### ttk Style layer
|
|
51
|
+
|
|
52
|
+
- [ ] `Layout.style("my_button", background=..., font=...)` style definitions
|
|
53
|
+
|
|
54
|
+
### Declarative components
|
|
55
|
+
|
|
56
|
+
- [ ] `@app.component` decorator (React-like reuse, replacing `Layout`)
|
|
57
|
+
- [ ] State type definitions and validation (no Pydantic)
|
|
58
|
+
|
|
59
|
+
### Testing
|
|
60
|
+
|
|
61
|
+
- [ ] WidgetSpec unit tests
|
|
@@ -268,11 +268,17 @@ class TkApp:
|
|
|
268
268
|
*,
|
|
269
269
|
debug: bool = False,
|
|
270
270
|
theme: bool | str = "kizashi",
|
|
271
|
+
ingest_trace: bool = False,
|
|
271
272
|
):
|
|
272
273
|
self._title = title
|
|
273
274
|
self._debug = debug
|
|
274
275
|
self._theme = self._normalize_theme(theme)
|
|
275
276
|
self._kizashi = self._theme == "kizashi"
|
|
277
|
+
# When True, a ``trace_add("write", ...)`` is installed on every
|
|
278
|
+
# registered Tcl variable. User edits (e.g. typing into an entry)
|
|
279
|
+
# are ingested back into ``state`` (kept as the single source of
|
|
280
|
+
# truth) and batched into a single sync pass at the next idle event.
|
|
281
|
+
self._ingest_trace = ingest_trace
|
|
276
282
|
self._acc_supported: bool | None = None
|
|
277
283
|
self._a11y_last_toggle: dict[str, str] = {}
|
|
278
284
|
self._widgets: list[WidgetSpec] = []
|
|
@@ -280,6 +286,15 @@ class TkApp:
|
|
|
280
286
|
self._root: tk.Tk | None = None
|
|
281
287
|
self._tk_widgets: dict[str, tk.Widget] = {}
|
|
282
288
|
self._tk_vars: dict[str, tk.Variable] = {}
|
|
289
|
+
# Re-entrancy guard: while the framework itself calls ``var.set``
|
|
290
|
+
# (from apply_state), a write trace must NOT re-ingest that value.
|
|
291
|
+
self._syncing_var_keys: set[str] = set()
|
|
292
|
+
# Keys whose Tcl var changed from a user edit, awaiting a batched flush.
|
|
293
|
+
self._pending_ingest: set[str] = set()
|
|
294
|
+
self._ingest_flush_job: str | None = None
|
|
295
|
+
# True while widgets are being built; build-time ``var.set`` calls
|
|
296
|
+
# (e.g. placeholders) must not be ingested into state.
|
|
297
|
+
self._building = False
|
|
283
298
|
self._widget_masters: dict[str, tk.Misc] = {}
|
|
284
299
|
self._row_pack_jobs: list[tuple[tk.Frame, Any]] = []
|
|
285
300
|
self._grid_pack_jobs: list[tuple[tk.Frame, Any]] = []
|
|
@@ -356,7 +371,11 @@ class TkApp:
|
|
|
356
371
|
self._text_inner.clear()
|
|
357
372
|
self._text_scrollbars.clear()
|
|
358
373
|
self._text_scroll_sync.clear()
|
|
374
|
+
self._text_set_hooks.clear()
|
|
359
375
|
self._a11y_last_toggle.clear()
|
|
376
|
+
self._pending_ingest.clear()
|
|
377
|
+
self._ingest_flush_job = None
|
|
378
|
+
self._syncing_var_keys.clear()
|
|
360
379
|
self._current_view = None
|
|
361
380
|
self._current_stage = None
|
|
362
381
|
self._swap_targets.clear()
|
|
@@ -1289,7 +1308,7 @@ class TkApp:
|
|
|
1289
1308
|
layout, row_jobs, grid_jobs = jobs_by_view[view]
|
|
1290
1309
|
layout.pack_children_for(self, row_jobs, grid_jobs)
|
|
1291
1310
|
else:
|
|
1292
|
-
self.pack_view_widgets(view, center_kinds=centered, fill="x", pady=
|
|
1311
|
+
self.pack_view_widgets(view, center_kinds=centered, fill="x", pady=t.SPACE[1])
|
|
1293
1312
|
|
|
1294
1313
|
def _on_tab_changed(_event: tk.Event[tk.Misc] | None = None) -> None:
|
|
1295
1314
|
current = nb.select()
|
|
@@ -1461,7 +1480,7 @@ class TkApp:
|
|
|
1461
1480
|
layout, row_jobs, grid_jobs = jobs_by_stage[stage]
|
|
1462
1481
|
layout.pack_children_for(self, row_jobs, grid_jobs)
|
|
1463
1482
|
else:
|
|
1464
|
-
self.pack_view_widgets(stage, center_kinds=centered, fill="x", pady=
|
|
1483
|
+
self.pack_view_widgets(stage, center_kinds=centered, fill="x", pady=t.SPACE[1])
|
|
1465
1484
|
|
|
1466
1485
|
active = str(self._state.get(key, stages[0] if stages else ""))
|
|
1467
1486
|
if active not in stages:
|
|
@@ -2608,7 +2627,7 @@ class TkApp:
|
|
|
2608
2627
|
"""
|
|
2609
2628
|
width = options.get("width", 300)
|
|
2610
2629
|
height = options.get("height", 200)
|
|
2611
|
-
bg = options.get("bg",
|
|
2630
|
+
bg = options.get("bg", t.SURFACE)
|
|
2612
2631
|
description = options.get("description")
|
|
2613
2632
|
items = options.get("items")
|
|
2614
2633
|
takefocus = options.get("takefocus")
|
|
@@ -2657,15 +2676,21 @@ class TkApp:
|
|
|
2657
2676
|
update_to_apply.pop(stage_key, None)
|
|
2658
2677
|
|
|
2659
2678
|
self._state.update(update_to_apply)
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2679
|
+
# Mark the keys being written by the framework so their write traces
|
|
2680
|
+
# (if ingest_trace is on) do not loop the value back into state.
|
|
2681
|
+
self._syncing_var_keys.update(update_to_apply)
|
|
2682
|
+
try:
|
|
2683
|
+
for key, val in update_to_apply.items():
|
|
2684
|
+
var = self._tk_vars.get(key)
|
|
2685
|
+
if var is None:
|
|
2686
|
+
continue
|
|
2687
|
+
s = "" if val is None else str(val)
|
|
2688
|
+
var.set(s)
|
|
2689
|
+
w = self._tk_widgets.get(key)
|
|
2690
|
+
if isinstance(w, (tk.Entry, ttk.Entry)):
|
|
2691
|
+
self._apply_entry_value_after_state(key, w, var, s)
|
|
2692
|
+
finally:
|
|
2693
|
+
self._syncing_var_keys.difference_update(update_to_apply)
|
|
2669
2694
|
if full:
|
|
2670
2695
|
self._sync_widgets()
|
|
2671
2696
|
else:
|
|
@@ -2690,6 +2715,61 @@ class TkApp:
|
|
|
2690
2715
|
# Keep state in sync with the rendered stage.
|
|
2691
2716
|
self._state[key] = new_stage
|
|
2692
2717
|
|
|
2718
|
+
def _register_var(self, key: str, var: tk.Variable) -> None:
|
|
2719
|
+
"""Register a Tcl variable under *key*, installing an ingest trace.
|
|
2720
|
+
|
|
2721
|
+
When ``ingest_trace=True``, a ``write`` trace feeds ``var.get()`` back
|
|
2722
|
+
into ``state[key]`` so user edits (typing into an entry, toggling a
|
|
2723
|
+
checkbutton, dragging a scale) become first-class state transitions.
|
|
2724
|
+
Framework-originated writes (from ``apply_state``) are excluded via
|
|
2725
|
+
``_syncing_var_keys`` so they do not loop back. Multiple edits are
|
|
2726
|
+
coalesced into one ``after_idle`` sync pass.
|
|
2727
|
+
"""
|
|
2728
|
+
self._tk_vars[key] = var
|
|
2729
|
+
if self._ingest_trace:
|
|
2730
|
+
try:
|
|
2731
|
+
var.trace_add(
|
|
2732
|
+
"write",
|
|
2733
|
+
lambda _n, _i, _op, k=key: self._on_var_ingest(k),
|
|
2734
|
+
)
|
|
2735
|
+
except tk.TclError:
|
|
2736
|
+
# Variable may already be deleted; ingest is best-effort.
|
|
2737
|
+
pass
|
|
2738
|
+
|
|
2739
|
+
def _on_var_ingest(self, key: str) -> None:
|
|
2740
|
+
"""Trace callback: a user edit changed ``var``; queue it for ingest."""
|
|
2741
|
+
# Skip writes that the framework itself issued from apply_state, and
|
|
2742
|
+
# any writes that happen while widgets are being built (placeholders).
|
|
2743
|
+
if key in self._syncing_var_keys or self._building:
|
|
2744
|
+
return
|
|
2745
|
+
self._pending_ingest.add(key)
|
|
2746
|
+
if self._root is None:
|
|
2747
|
+
return
|
|
2748
|
+
if self._ingest_flush_job is None:
|
|
2749
|
+
try:
|
|
2750
|
+
self._ingest_flush_job = self._root.after_idle(self._flush_ingest)
|
|
2751
|
+
except tk.TclError:
|
|
2752
|
+
self._ingest_flush_job = None
|
|
2753
|
+
|
|
2754
|
+
def _flush_ingest(self) -> None:
|
|
2755
|
+
"""Flush queued user edits into ``state`` in one batched pass."""
|
|
2756
|
+
self._ingest_flush_job = None
|
|
2757
|
+
if not self._pending_ingest:
|
|
2758
|
+
return
|
|
2759
|
+
pending = self._pending_ingest
|
|
2760
|
+
self._pending_ingest = set()
|
|
2761
|
+
update: dict[str, Any] = {}
|
|
2762
|
+
for key in pending:
|
|
2763
|
+
var = self._tk_vars.get(key)
|
|
2764
|
+
if var is None:
|
|
2765
|
+
continue
|
|
2766
|
+
try:
|
|
2767
|
+
update[key] = var.get()
|
|
2768
|
+
except tk.TclError:
|
|
2769
|
+
continue
|
|
2770
|
+
if update:
|
|
2771
|
+
self._apply_state_dict(update, full=False)
|
|
2772
|
+
|
|
2693
2773
|
def _known_state_keys(self) -> set[str]:
|
|
2694
2774
|
"""Return the set of state keys that have meaning in the app."""
|
|
2695
2775
|
keys: set[str] = set()
|
|
@@ -3345,6 +3425,19 @@ class TkApp:
|
|
|
3345
3425
|
|
|
3346
3426
|
self._first_focusable: tk.Widget | None = None
|
|
3347
3427
|
|
|
3428
|
+
self._building = True
|
|
3429
|
+
try:
|
|
3430
|
+
self._build_widgets_inner()
|
|
3431
|
+
finally:
|
|
3432
|
+
self._building = False
|
|
3433
|
+
|
|
3434
|
+
def _build_widgets_inner(self) -> None:
|
|
3435
|
+
"""Create tkinter widgets from registered specs (inside build guard)."""
|
|
3436
|
+
if self._root is None:
|
|
3437
|
+
return
|
|
3438
|
+
|
|
3439
|
+
self._first_focusable: tk.Widget | None = None
|
|
3440
|
+
|
|
3348
3441
|
for spec in self._widgets:
|
|
3349
3442
|
if spec.kind == "paned":
|
|
3350
3443
|
master = self._widget_masters.get(spec.name, self._root)
|
|
@@ -3745,7 +3838,7 @@ class TkApp:
|
|
|
3745
3838
|
)
|
|
3746
3839
|
w = ttk.Entry(master, textvariable=var, style=style)
|
|
3747
3840
|
self._tk_widgets[spec.name] = w
|
|
3748
|
-
self.
|
|
3841
|
+
self._register_var(spec.name, var)
|
|
3749
3842
|
for opt in ("show", "width", "state"):
|
|
3750
3843
|
if opt in e:
|
|
3751
3844
|
w.configure(**{opt: e[opt]})
|
|
@@ -3791,7 +3884,7 @@ class TkApp:
|
|
|
3791
3884
|
# anchor is a ttk layout option (style.layout), not a constructor kwarg
|
|
3792
3885
|
# for ttk.Checkbutton; left alignment is enforced by the Kizashi style.
|
|
3793
3886
|
self._tk_widgets[spec.name] = w
|
|
3794
|
-
self.
|
|
3887
|
+
self._register_var(key, var)
|
|
3795
3888
|
if spec.on_update is not None:
|
|
3796
3889
|
fn = spec.on_update
|
|
3797
3890
|
w.configure(command=lambda s=spec, f=fn, v=var, k=key:
|
|
@@ -3802,7 +3895,7 @@ class TkApp:
|
|
|
3802
3895
|
gk = e.get("group_key", "radio")
|
|
3803
3896
|
val = e.get("rb_value", "")
|
|
3804
3897
|
if gk not in self._tk_vars:
|
|
3805
|
-
self.
|
|
3898
|
+
self._register_var(gk, tk.StringVar(value=""))
|
|
3806
3899
|
var = self._tk_vars[gk]
|
|
3807
3900
|
# padding is owned by the theme style (44px min target size)
|
|
3808
3901
|
style = "TRadiobutton"
|
|
@@ -3932,7 +4025,7 @@ class TkApp:
|
|
|
3932
4025
|
length=e.get("length", 200 if orient_str == "horizontal" else 100),
|
|
3933
4026
|
) # type: ignore[arg-type]
|
|
3934
4027
|
self._tk_widgets[spec.name] = w
|
|
3935
|
-
self.
|
|
4028
|
+
self._register_var(key, var)
|
|
3936
4029
|
self._state[key] = str(var.get())
|
|
3937
4030
|
if spec.on_update is not None:
|
|
3938
4031
|
fn = spec.on_update
|
|
@@ -3963,7 +4056,7 @@ class TkApp:
|
|
|
3963
4056
|
kwargs["font"] = e["font"]
|
|
3964
4057
|
w = ttk.Spinbox(master, textvariable=var, **kwargs)
|
|
3965
4058
|
self._tk_widgets[spec.name] = w
|
|
3966
|
-
self.
|
|
4059
|
+
self._register_var(key, var)
|
|
3967
4060
|
if init_val:
|
|
3968
4061
|
self._state[key] = init_val
|
|
3969
4062
|
if spec.on_update is not None:
|
|
@@ -3989,7 +4082,7 @@ class TkApp:
|
|
|
3989
4082
|
kwargs["font"] = e["font"]
|
|
3990
4083
|
w = ttk.Combobox(master, **kwargs)
|
|
3991
4084
|
self._tk_widgets[spec.name] = w
|
|
3992
|
-
self.
|
|
4085
|
+
self._register_var(key, var)
|
|
3993
4086
|
if init_val:
|
|
3994
4087
|
self._state[key] = init_val
|
|
3995
4088
|
if spec.on_update is not None:
|
|
@@ -4000,7 +4093,7 @@ class TkApp:
|
|
|
4000
4093
|
def _build_listbox(self, spec: WidgetSpec, master: tk.Misc) -> None:
|
|
4001
4094
|
e = spec.extras
|
|
4002
4095
|
kwargs_lb: dict[str, Any] = {
|
|
4003
|
-
"bg": t.
|
|
4096
|
+
"bg": t.BG,
|
|
4004
4097
|
"fg": t.TEXT,
|
|
4005
4098
|
"selectbackground": t.ACCENT_RAMP[200],
|
|
4006
4099
|
"selectforeground": t.ACCENT_RAMP[700],
|
|
@@ -783,6 +783,25 @@ def _place_paired_with_gutters(
|
|
|
783
783
|
_bind_gutter_edit_sync(app, text_b, gutter_b)
|
|
784
784
|
|
|
785
785
|
|
|
786
|
+
def _logical_line_count(text: tk.Text) -> int:
|
|
787
|
+
"""Return the number of logical (non-wrapped) lines in *text*.
|
|
788
|
+
|
|
789
|
+
``index("end-1c")`` counts *display* lines: a long line that wraps to
|
|
790
|
+
several rows inflates the count when ``wrap`` is ``word``/``char``. We
|
|
791
|
+
count newline characters instead, which is wrap-independent. The last
|
|
792
|
+
logical line may or may not carry a trailing newline (``text_set`` omits
|
|
793
|
+
it; a hand-inserted buffer may keep it), so the count is adjusted by
|
|
794
|
+
whether the content ends with a newline.
|
|
795
|
+
"""
|
|
796
|
+
try:
|
|
797
|
+
content = text.get("1.0", "end-1c")
|
|
798
|
+
except tk.TclError:
|
|
799
|
+
return 0
|
|
800
|
+
if not content:
|
|
801
|
+
return 0
|
|
802
|
+
return content.count("\n") + (0 if content.endswith("\n") else 1)
|
|
803
|
+
|
|
804
|
+
|
|
786
805
|
def _reconcile_gutter(gutter: tk.Text, text: tk.Text) -> None:
|
|
787
806
|
"""Fill a gutter with logical line numbers, matching the text line count.
|
|
788
807
|
|
|
@@ -790,17 +809,26 @@ def _reconcile_gutter(gutter: tk.Text, text: tk.Text) -> None:
|
|
|
790
809
|
gutter is rewritten so its scroll range matches the pane. Idempotent:
|
|
791
810
|
rewriting only happens when the count differs.
|
|
792
811
|
"""
|
|
793
|
-
|
|
812
|
+
# Re-entry guard: rewriting the gutter fires yscrollcommand on the shared
|
|
813
|
+
# scrollbar (via update_idletasks below), which re-enters this function
|
|
814
|
+
# through _chain_yview. Without a guard that recursion never terminates.
|
|
815
|
+
if getattr(gutter, "_syncing_gutter", False):
|
|
816
|
+
return
|
|
817
|
+
n = _logical_line_count(text)
|
|
794
818
|
gutter_lines = int(gutter.index("end-1c").split(".")[0])
|
|
795
819
|
if gutter_lines == n:
|
|
796
820
|
return
|
|
797
821
|
lines = "\n".join(str(i) for i in range(1, n + 1))
|
|
798
|
-
gutter.
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
822
|
+
gutter._syncing_gutter = True # type: ignore[attr-defined]
|
|
823
|
+
try:
|
|
824
|
+
gutter.configure(state="normal")
|
|
825
|
+
gutter.delete("1.0", "end")
|
|
826
|
+
gutter.insert("1.0", lines)
|
|
827
|
+
gutter.configure(state="disabled")
|
|
828
|
+
# Ensure the new content is reflected in the shared scrollbar range.
|
|
829
|
+
gutter.update_idletasks()
|
|
830
|
+
finally:
|
|
831
|
+
gutter._syncing_gutter = False # type: ignore[attr-defined]
|
|
804
832
|
|
|
805
833
|
|
|
806
834
|
def _chain_yview(
|
|
@@ -857,14 +885,20 @@ def _make_gutter(parent: tk.Misc, name: str) -> tk.Text:
|
|
|
857
885
|
|
|
858
886
|
def _populate_gutters(gutter: tk.Text, text: tk.Text) -> None:
|
|
859
887
|
"""Fill a gutter with logical line numbers (1..n)."""
|
|
860
|
-
|
|
888
|
+
if getattr(gutter, "_syncing_gutter", False):
|
|
889
|
+
return
|
|
890
|
+
n = _logical_line_count(text)
|
|
861
891
|
lines = "\n".join(str(i) for i in range(1, n + 1))
|
|
862
|
-
gutter.
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
892
|
+
gutter._syncing_gutter = True # type: ignore[attr-defined]
|
|
893
|
+
try:
|
|
894
|
+
gutter.configure(state="normal")
|
|
895
|
+
gutter.delete("1.0", "end")
|
|
896
|
+
gutter.insert("1.0", lines)
|
|
897
|
+
gutter.configure(state="disabled")
|
|
898
|
+
# Ensure the new content is reflected in the shared scrollbar range.
|
|
899
|
+
gutter.update_idletasks()
|
|
900
|
+
finally:
|
|
901
|
+
gutter._syncing_gutter = False # type: ignore[attr-defined]
|
|
868
902
|
|
|
869
903
|
|
|
870
904
|
def _bind_gutter_edit_sync(app: TkApp, text: tk.Text, gutter: tk.Text) -> None:
|
|
@@ -1347,6 +1381,14 @@ class Layout:
|
|
|
1347
1381
|
the gutters always stay in lock-step with the content (no drift). Line
|
|
1348
1382
|
numbers are logical rows of the text widget (``1..n``).
|
|
1349
1383
|
|
|
1384
|
+
.. note::
|
|
1385
|
+
``line_numbers=True`` takes precedence over ``sync_yscroll``: the
|
|
1386
|
+
shared-scrollbar layout it installs always keeps the two panes (and
|
|
1387
|
+
both gutters) scrolled in lock-step, so passing
|
|
1388
|
+
``sync_yscroll=False`` with ``line_numbers=True`` has no effect. If
|
|
1389
|
+
you need the panes to scroll independently, do **not** enable
|
|
1390
|
+
``line_numbers``.
|
|
1391
|
+
|
|
1350
1392
|
Example::
|
|
1351
1393
|
|
|
1352
1394
|
app.text("left", readonly=True, sync_yscroll_with="right")
|
|
@@ -60,7 +60,7 @@ def apply_theme(root):
|
|
|
60
60
|
# --- entries ------------------------------------------------------------
|
|
61
61
|
style.configure(
|
|
62
62
|
"TEntry",
|
|
63
|
-
fieldbackground=t.
|
|
63
|
+
fieldbackground=t.BG,
|
|
64
64
|
foreground=t.TEXT,
|
|
65
65
|
bordercolor=t.DIVIDER,
|
|
66
66
|
lightcolor=t.DIVIDER,
|
|
@@ -225,6 +225,11 @@ def apply_theme(root):
|
|
|
225
225
|
font=t.font("body"),
|
|
226
226
|
focuscolor=t.FOCUS,
|
|
227
227
|
padding=_check_radio_padding,
|
|
228
|
+
# clam's radiobutton gap between indicator and label is driven by
|
|
229
|
+
# ``indicatormargin`` (indicatorpadding is ignored by clam). Widen the
|
|
230
|
+
# right margin to SPACE[2] (8px) to match TCheckbutton, so the symbol
|
|
231
|
+
# and label read as separate without crowding.
|
|
232
|
+
indicatormargin=(0, 0, t.SPACE[2], 0),
|
|
228
233
|
)
|
|
229
234
|
style.layout(
|
|
230
235
|
"TRadiobutton",
|
|
@@ -252,20 +257,30 @@ def apply_theme(root):
|
|
|
252
257
|
)
|
|
253
258
|
|
|
254
259
|
# --- scale --------------------------------------------------------------
|
|
260
|
+
# "Card" thumb: page-ground (BG) fill with an ACCENT border. The border
|
|
261
|
+
# provides the non-text contrast needed to pick the thumb out of the
|
|
262
|
+
# SURFACE trough (~5.3:1, WCAG 1.4.11); the BG fill against the ACCENT
|
|
263
|
+
# border stays legible too (~5.9:1). Hover/pressed shift the border.
|
|
255
264
|
style.configure(
|
|
256
265
|
"TScale",
|
|
257
266
|
background=t.BG,
|
|
258
267
|
troughcolor=t.SURFACE,
|
|
259
|
-
bordercolor=t.
|
|
260
|
-
lightcolor=t.
|
|
261
|
-
darkcolor=t.
|
|
268
|
+
bordercolor=t.ACCENT,
|
|
269
|
+
lightcolor=t.ACCENT,
|
|
270
|
+
darkcolor=t.ACCENT,
|
|
262
271
|
padding=(t.SPACE[1], t.SPACE[2]),
|
|
263
272
|
)
|
|
273
|
+
style.map(
|
|
274
|
+
"TScale",
|
|
275
|
+
bordercolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
|
|
276
|
+
lightcolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
|
|
277
|
+
darkcolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
|
|
278
|
+
)
|
|
264
279
|
|
|
265
280
|
# --- spinbox ------------------------------------------------------------
|
|
266
281
|
style.configure(
|
|
267
282
|
"TSpinbox",
|
|
268
|
-
fieldbackground=t.
|
|
283
|
+
fieldbackground=t.BG,
|
|
269
284
|
foreground=t.TEXT,
|
|
270
285
|
bordercolor=t.DIVIDER,
|
|
271
286
|
lightcolor=t.DIVIDER,
|
|
@@ -287,7 +302,7 @@ def apply_theme(root):
|
|
|
287
302
|
# --- combobox -----------------------------------------------------------
|
|
288
303
|
style.configure(
|
|
289
304
|
"TCombobox",
|
|
290
|
-
fieldbackground=t.
|
|
305
|
+
fieldbackground=t.BG,
|
|
291
306
|
foreground=t.TEXT,
|
|
292
307
|
bordercolor=t.DIVIDER,
|
|
293
308
|
lightcolor=t.DIVIDER,
|
|
@@ -304,7 +319,7 @@ def apply_theme(root):
|
|
|
304
319
|
lightcolor=[("focus", t.FOCUS)],
|
|
305
320
|
darkcolor=[("focus", t.FOCUS)],
|
|
306
321
|
arrowcolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
|
|
307
|
-
fieldbackground=[("readonly", t.
|
|
322
|
+
fieldbackground=[("readonly", t.BG), ("active", t.BG)],
|
|
308
323
|
selectbackground=[("focus", t.ACCENT_RAMP[200])],
|
|
309
324
|
selectforeground=[("focus", t.TEXT)],
|
|
310
325
|
)
|
|
@@ -316,7 +331,7 @@ def apply_theme(root):
|
|
|
316
331
|
# foreground so the current value stays readable.
|
|
317
332
|
style.configure(
|
|
318
333
|
"Readonly.TCombobox",
|
|
319
|
-
fieldbackground=t.
|
|
334
|
+
fieldbackground=t.BG,
|
|
320
335
|
foreground=t.TEXT,
|
|
321
336
|
bordercolor=t.DIVIDER,
|
|
322
337
|
lightcolor=t.DIVIDER,
|
|
@@ -333,7 +348,7 @@ def apply_theme(root):
|
|
|
333
348
|
lightcolor=[("focus", t.FOCUS)],
|
|
334
349
|
darkcolor=[("focus", t.FOCUS)],
|
|
335
350
|
arrowcolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
|
|
336
|
-
fieldbackground=[("readonly", t.
|
|
351
|
+
fieldbackground=[("readonly", t.BG), ("active", t.BG)],
|
|
337
352
|
selectbackground=[("focus", t.ACCENT_RAMP[300])],
|
|
338
353
|
selectforeground=[("focus", t.TEXT)],
|
|
339
354
|
foreground=[("readonly", t.TEXT), ("active", t.TEXT)],
|
|
@@ -355,18 +370,22 @@ def apply_theme(root):
|
|
|
355
370
|
)
|
|
356
371
|
|
|
357
372
|
# --- scrollbar ----------------------------------------------------------
|
|
373
|
+
# Thumb matches the scale thumb: BG fill with an ACCENT border on a
|
|
374
|
+
# SURFACE trough. The border gives the non-text contrast to pick the
|
|
375
|
+
# thumb out of the trough; hover/pressed shift the border accent ramp.
|
|
358
376
|
style.configure(
|
|
359
377
|
"TScrollbar",
|
|
360
|
-
background=t.
|
|
378
|
+
background=t.BG,
|
|
361
379
|
troughcolor=t.SURFACE,
|
|
362
|
-
bordercolor=t.
|
|
380
|
+
bordercolor=t.ACCENT,
|
|
363
381
|
arrowcolor=t.TEXT,
|
|
364
382
|
gripcount=0,
|
|
365
383
|
borderwidth=0,
|
|
366
384
|
)
|
|
367
385
|
style.map(
|
|
368
386
|
"TScrollbar",
|
|
369
|
-
|
|
387
|
+
bordercolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
|
|
388
|
+
background=[("active", t.NEUTRAL[100]), ("pressed", t.NEUTRAL[200])],
|
|
370
389
|
arrowcolor=[("active", t.TEXT), ("pressed", t.TEXT)],
|
|
371
390
|
)
|
|
372
391
|
|
|
@@ -538,23 +557,35 @@ def field_row(parent, row, label_text, textvariable, *, label_width=None, entry_
|
|
|
538
557
|
|
|
539
558
|
def window_header(parent, title, subtitle=None):
|
|
540
559
|
"""Standard top-of-window block: left-aligned title, optional muted
|
|
541
|
-
subtitle
|
|
542
|
-
|
|
560
|
+
subtitle. Hierarchy comes from type scale and spacing, not from boxes
|
|
561
|
+
or divider lines (Kizashi design principle).
|
|
543
562
|
|
|
544
563
|
Returns (title_label, subtitle_label_or_None).
|
|
545
564
|
"""
|
|
546
|
-
|
|
565
|
+
header_bg = t.BG # "#faf8f4" — same as page background
|
|
566
|
+
|
|
567
|
+
# Title: Kizashi h4 with space-2 (16px) internal padding for consistent
|
|
568
|
+
# background coverage. No bottom margin — spacing comes from subtitle.
|
|
569
|
+
title_lbl = tk.Label(parent, text=title, bg=header_bg, fg=t.TEXT,
|
|
547
570
|
font=t.font("h4"), anchor="w",
|
|
548
|
-
bd=0, highlightthickness=0
|
|
571
|
+
bd=0, highlightthickness=0,
|
|
572
|
+
padx=t.SPACE[2], pady=t.SPACE[2])
|
|
549
573
|
title_lbl.pack(fill="x")
|
|
574
|
+
|
|
550
575
|
sub_lbl = None
|
|
551
576
|
if subtitle is not None:
|
|
552
|
-
|
|
577
|
+
# Kizashi small text with space-2 padding.
|
|
578
|
+
# Top margin space-2 (16px) creates the gap from title.
|
|
579
|
+
sub_lbl = tk.Label(parent, text=subtitle, bg=header_bg, fg=t.TEXT_MUTED,
|
|
553
580
|
font=t.font("small"), anchor="w",
|
|
554
|
-
bd=0, highlightthickness=0
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
581
|
+
bd=0, highlightthickness=0,
|
|
582
|
+
padx=t.SPACE[2], pady=t.SPACE[2])
|
|
583
|
+
sub_lbl.pack(fill="x")
|
|
584
|
+
|
|
585
|
+
# Add space-4 (32px) below the header to separate from the first section
|
|
586
|
+
# (Kizashi section spacing principle).
|
|
587
|
+
tk.Frame(parent, height=t.SPACE[4], bg=header_bg).pack(fill="x")
|
|
588
|
+
|
|
558
589
|
return title_lbl, sub_lbl
|
|
559
590
|
|
|
560
591
|
|
|
@@ -581,7 +612,7 @@ def data_list(parent, columns, height=12, selectmode: str = "browse"):
|
|
|
581
612
|
else:
|
|
582
613
|
tree.column(key, width=width, minwidth=width, anchor=anchor, stretch=False)
|
|
583
614
|
|
|
584
|
-
tree.tag_configure("odd", background=t.
|
|
615
|
+
tree.tag_configure("odd", background=t.BG)
|
|
585
616
|
tree.tag_configure("even", background=t.BG)
|
|
586
617
|
|
|
587
618
|
sb = ttk.Scrollbar(container, orient="vertical", command=tree.yview)
|
|
@@ -605,17 +636,16 @@ def clear_rows(tree):
|
|
|
605
636
|
|
|
606
637
|
|
|
607
638
|
def status_bar(root, textvariable=None, text=""):
|
|
608
|
-
"""Bottom status bar: a
|
|
639
|
+
"""Bottom status bar: a left-aligned muted line.
|
|
609
640
|
|
|
610
641
|
Use for hints and state instead of centered label boxes.
|
|
642
|
+
Hierarchy comes from spacing and typography (Kizashi principle).
|
|
611
643
|
Returns the label.
|
|
612
644
|
"""
|
|
613
645
|
bar = tk.Frame(root, bg=t.BG, bd=0, highlightthickness=0)
|
|
614
646
|
bar.pack(side="bottom", fill="x")
|
|
615
|
-
rule = tk.Frame(bar, height=2, bg=t.DIVIDER, bd=0, highlightthickness=0)
|
|
616
|
-
rule.pack(fill="x")
|
|
617
647
|
lbl = tk.Label(bar, bg=t.BG, fg=t.TEXT_MUTED, font=t.font("small"),
|
|
618
|
-
anchor="w", padx=t.SPACE[
|
|
648
|
+
anchor="w", padx=t.SPACE[2], pady=t.SPACE[2],
|
|
619
649
|
bd=0, highlightthickness=0)
|
|
620
650
|
if textvariable is not None:
|
|
621
651
|
lbl.configure(textvariable=textvariable)
|
|
@@ -22,7 +22,7 @@ import tkinter.font as tkfont
|
|
|
22
22
|
# Contrast pairs are regression-tested in tests/test_tokens.py
|
|
23
23
|
# (WCAG 1.4.3 Contrast (Minimum), 1.4.11 Non-text Contrast).
|
|
24
24
|
BG = "#faf8f4" # Background (page ground)
|
|
25
|
-
SURFACE = "#
|
|
25
|
+
SURFACE = "#f1ece3" # Surface (fields, panels) — Kizashi spec
|
|
26
26
|
CARD = "#ffffff" # Card background (design uses pure white on BG)
|
|
27
27
|
TEXT = "#302b24" # Text Primary
|
|
28
28
|
TEXT_SECONDARY = "#4a4339" # Paragraph text on cards/sections
|
nextpytk-0.4.6/ROADMAP.md
DELETED
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
# ROADMAP — nextpytk
|
|
2
|
-
|
|
3
|
-
## Snapshot (v0.4.0)
|
|
4
|
-
|
|
5
|
-
### Implemented widgets (`@app.*`)
|
|
6
|
-
|
|
7
|
-
| kind | Notes |
|
|
8
|
-
|------|-------|
|
|
9
|
-
| `label` / `status` / `message` | Display / word-wrap. `status` sets `role=status` metadata (not a live region yet) |
|
|
10
|
-
| `button` / `entry` | ttk, `state=` support |
|
|
11
|
-
| `checkbutton` / `radiobutton` / `scale` / `spinbox` | ttk |
|
|
12
|
-
| `combobox` | ttk.Combobox (`readonly`, `values`) |
|
|
13
|
-
| `menubar` | Window menubar with cascades / `enabled_if` |
|
|
14
|
-
| `text` / `listbox` / `canvas` | tk (Text: tags, readonly, paired y-scroll sync) |
|
|
15
|
-
| `treeview` | Multi-column (`show="headings"`) |
|
|
16
|
-
| `paned` | `ttk.Panedwindow` + `with app.pane(...)` |
|
|
17
|
-
| `progressbar` | `state[key]` / `{name}_running` for determinate/indeterminate |
|
|
18
|
-
| `bind` | Global shortcuts (annotates matching button) |
|
|
19
|
-
|
|
20
|
-
Infrastructure: `Layout` (pack/grid, `.header()` / `.status()` chrome), `@app.multiview`, `@app.stages` / `run(stages=..., tabposition=...)`, `run` / `run_async` / `spawn` / `@app.job`, `schema()`, `debug_layout()`, builder registry, a11y choke point `_apply_a11y`.
|
|
21
|
-
|
|
22
|
-
Theme: `tokens` + `apply_theme` / layout helpers (contrast + 44px target-size regressions).
|
|
23
|
-
|
|
24
|
-
PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
25
|
-
|
|
26
|
-
### Examples
|
|
27
|
-
|
|
28
|
-
- `disk_usage_flat_async.py` — async ncdu-style viewer (`run_async` + `spawn`)
|
|
29
|
-
- `combobox_demo.py`, `menubar_demo.py`, `header_demo.py`, `text_sync_demo.py`
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Snapshot (v0.4.1) — post-0.4 fixes
|
|
34
|
-
|
|
35
|
-
### Text widget ergonomics
|
|
36
|
-
|
|
37
|
-
- `app.text_widget(name)` returns the real `tk.Text` instance (the outer container is still available via `app.widget(name)`).
|
|
38
|
-
- `app.text_get(name)` returns the current full contents.
|
|
39
|
-
- `text` widgets now sync from state via `apply_state({name: ...})`, including read-only panes.
|
|
40
|
-
- `_on_text_change()` now reads from the real `tk.Text` instead of the empty container frame.
|
|
41
|
-
- A11y traits now attach to the real `tk.Text` rather than the container frame.
|
|
42
|
-
|
|
43
|
-
### Layout documentation
|
|
44
|
-
|
|
45
|
-
- Horizontal splits are already supported via `app.paned(..., orient=Orient.HORIZONTAL)` and `Layout.paned(...)`; see `examples/paned_split.py` and `examples/text_sync_demo.py`.
|
|
46
|
-
- `on_ready` is intended for post-build setup, not reparenting already-packed widgets. Use up-front `paned` / `grid` / `section` declarations for dynamic-looking layouts.
|
|
47
|
-
- The reported layout issue was **not** a nextpytk bug: widgets were placed in row 0 but `row_weights(0, 1)` gave all vertical stretch to the empty row 1. The framework behaved as documented; the fix was on the application side to use `row_weight(0, 1)` or a `paned` layout. This motivated stronger README warnings and the deprecation of the index-order plural methods below.
|
|
48
|
-
|
|
49
|
-
### Deprecations in v0.4.1 (removal in v0.5.0)
|
|
50
|
-
|
|
51
|
-
- `col_weights(*weights)`, `row_weights(*weights)`, `col_minsizes(*sizes)`, and `row_minsizes(*sizes)` on `_GridBuilder` are now deprecated. They assigned weights by index position, which was easy to misuse.
|
|
52
|
-
- `LayoutBuilder.grid(col_weights=..., row_weights=...)` keyword arguments are also deprecated.
|
|
53
|
-
- Migrate to the explicit singular API: `col_weight(col, weight)`, `row_weight(row, weight)`, `col_minsize(col, size)`, `row_minsize(row, size)`.
|
|
54
|
-
|
|
55
|
-
### Design system documentation (v0.4.1)
|
|
56
|
-
|
|
57
|
-
- Added a "Design System" section to README covering `Fill`/`Sticky`/`Orient` typed constants, `SPACE` token usage, and theme helpers.
|
|
58
|
-
- Documented the `SPACE` scale (`1:4, 2:8, 3:12, 4:16, 6:24, 8:32`) and the rule of using tokens instead of bare integers for padding/spacing.
|
|
59
|
-
- Updated `examples/grid_temp.py` and `examples/widget_gallery.py` to use the singular weight/min size API and `SPACE` tokens.
|
|
60
|
-
|
|
61
|
-
### Widget-level font and padding (v0.4.1)
|
|
62
|
-
|
|
63
|
-
- Added `font=` to `@app.button`, `@app.entry`, `@app.checkbutton`, `@app.radiobutton`, `@app.text`, `@app.spinbox`, `@app.listbox`, and `@app.combobox` (in addition to the existing `@app.label`).
|
|
64
|
-
- Added `padding=` to `@app.entry` so users can declaratively increase visual height (`ttk.Entry` does not expose `height` directly).
|
|
65
|
-
- For ttk widgets whose widget-level `configure()` rejects `-font` / `-padding` (Button, Entry, Checkbutton, Radiobutton), nextpytk now creates a derived `ttk.Style` per widget. The new style inherits the base style's layout and theme, applies only the requested overrides, and leaves other theme properties intact.
|
|
66
|
-
- Updated README.md, README.ja.md, and `tests/test_api.py` to cover the new options.
|
|
67
|
-
|
|
68
|
-
### Theme API redesign (v0.4.1)
|
|
69
|
-
|
|
70
|
-
- `TkApp(theme=...)` now accepts `bool | str`.
|
|
71
|
-
- `"kizashi"` (default) applies the built-in Kizashi design system.
|
|
72
|
-
- `"none"` leaves the platform default ttk theme untouched.
|
|
73
|
-
- Any other string switches to that installed ttk theme without Kizashi overrides.
|
|
74
|
-
- `theme=True` and `theme=False` are deprecated in v0.4.1 and will be removed in v0.5.0. Migrate to `"kizashi"` and `"none"`.
|
|
75
|
-
- Updated `README.md`, `README.ja.md`, and `tests/test_api.py`.
|
|
76
|
-
|
|
77
|
-
### Deferred
|
|
78
|
-
|
|
79
|
-
- `listbox` callback stays index-based by design (unifies with `treeview`, handles duplicate items, and avoids forcing the framework to reverse-map strings). Passing the display string would be a breaking change and is intentionally not pursued.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Snapshot (v0.4.2) — event-sequence generalization + listbox items_key
|
|
84
|
-
|
|
85
|
-
### Problem
|
|
86
|
-
|
|
87
|
-
`@app.bind(...)` accepts arbitrary Tk event strings, but the only typed constants lived under `nextpytk.types.ListboxEvent` (e.g. `ListboxEvent.RETURN`). Using a listbox-oriented name for a global shortcut or an entry-level Enter binding was confusing.
|
|
88
|
-
|
|
89
|
-
### Change
|
|
90
|
-
|
|
91
|
-
- Added a generic `nextpytk.types.EventSeq` class with common event sequences: `RETURN`, `ESCAPE`, `BACKSPACE`, `DELETE`, `TAB`, `DOUBLE_BUTTON_1`, `DOUBLE_BUTTON_2`, `DOUBLE_BUTTON_3`, `LISTBOX_SELECT`.
|
|
92
|
-
- Kept `ListboxEvent` as a backward-compatible subclass of `EventSeq` so existing code continues to work. Existing constants (`SELECT`, `RETURN`, `KEY_BACKSPACE`, `KEY_DELETE`) still resolve to the same string values. `DOUBLE_CLICK` is kept as a deprecated alias of `DOUBLE_BUTTON_1` and will be removed in v0.5.0.
|
|
93
|
-
- Updated `EventSeqLike` / `ListboxEventLike` to share the same literal union.
|
|
94
|
-
- Updated `app.py` docstrings for `@app.bind` and `@app.listbox` to reference `EventSeq` instead of `ListboxEvent`.
|
|
95
|
-
|
|
96
|
-
### listbox / combobox dynamic choices
|
|
97
|
-
|
|
98
|
-
- Added `items_key=` option to `@app.listbox` for state-driven list contents, symmetric to `treeview`'s existing `rows_key=`.
|
|
99
|
-
- Added `values_key=` option to `@app.combobox` for state-driven dropdown values, following the same widget-native naming principle.
|
|
100
|
-
- With these keys, `apply_state({items_key: [...]})` / `apply_state({values_key: [...]})` refresh the widget choices while keeping the selection in `state[name]` / `state[key]`.
|
|
101
|
-
- Static `items=` / `values=` remain available and still work as before. When both are provided, the dynamic key takes precedence at runtime.
|
|
102
|
-
- Full (`apply_state`) and partial (`_apply_state_dict`) updates both refresh the affected widgets when the dynamic key is touched.
|
|
103
|
-
- Selection is kept when it remains valid; if it becomes invalid (removed from values, out of range for listbox) it is reset to the widget's empty state.
|
|
104
|
-
|
|
105
|
-
### entry widget-level events
|
|
106
|
-
|
|
107
|
-
- Added `events=` option to `@app.entry` for widget-level event bindings.
|
|
108
|
-
- Accepts `dict[str, ListboxEventHandler]` mapping event sequences (e.g. `EventSeq.RETURN`) to handlers.
|
|
109
|
-
- Handlers receive a dict of current effective entry values (same shape as button callbacks) and may return a state update dict.
|
|
110
|
-
- Enables patterns like "press Enter in the search entry to trigger search" without a global `<Return>` bind or an application-level button.
|
|
111
|
-
|
|
112
|
-
### A11y-aware primary mouse-button helpers
|
|
113
|
-
|
|
114
|
-
- Added `EventSeq.PRIMARY_CLICK`, `EventSeq.PRIMARY_DOUBLE_CLICK`, and `EventSeq.PRIMARY_BUTTON_RELEASE` lazy descriptors to `nextpytk.types`. These return the tkinter event sequence for the OS-configured primary mouse button, accounting for left/right button swap on Windows, macOS, and Linux/GNOME. (The underlying `primary_click()`, `primary_double_click()`, and `primary_button_release()` functions are also available for direct use.)
|
|
115
|
-
- `_primary_button_number()` detects the swap setting per platform and caches the result with `functools.lru_cache(maxsize=1)` so repeated event lookups do not hit the OS each time.
|
|
116
|
-
- `EventSeq.PRIMARY_CLICK`, `EventSeq.PRIMARY_DOUBLE_CLICK`, and `EventSeq.PRIMARY_BUTTON_RELEASE` are lazy descriptors that re-evaluate on each access (the underlying `_primary_button_number()` is cached, so the cost is negligible). These are the recommended way to reference primary-button events in widget-level bindings (e.g. `events={EventSeq.PRIMARY_DOUBLE_CLICK: ...}` on `@app.listbox`).
|
|
117
|
-
- Updated `examples/disk_usage_flat_async.py` and the `@app.listbox` docstring to use `EventSeq.PRIMARY_DOUBLE_CLICK` instead of the deprecated `EventSeq.DOUBLE_CLICK`.
|
|
118
|
-
|
|
119
|
-
### Tests
|
|
120
|
-
|
|
121
|
-
- Added `tests/test_types.py` covering `DOUBLE_BUTTON_*`, the `DOUBLE_CLICK` alias, default primary-button behavior, and mocked Windows/macOS/GNOME left-handed settings returning the swapped button number.
|
|
122
|
-
|
|
123
|
-
### Design principle
|
|
124
|
-
|
|
125
|
-
- Use the tkinter-native vocabulary per widget: listbox has "items", treeview has "rows", combobox has "values". The corresponding dynamic keys are `items_key`, `rows_key`, and `values_key`. This is more intuitive than forcing a single generic name across widgets.
|
|
126
|
-
|
|
127
|
-
### Deprecation note
|
|
128
|
-
|
|
129
|
-
- `ListboxEvent` is soft-deprecated in v0.4.2. New code should use `EventSeq` directly. No removal timeline yet.
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## Snapshot (v0.4.3) — filepicker + layout blocks
|
|
134
|
-
|
|
135
|
-
- Added `@app.filepicker` (`filedialog` wrapper; `mode=` open/save/directory/open_multiple).
|
|
136
|
-
- Added `Layout(spacing=...)` for a layout-wide SPACE token default (padx/pady inheritance).
|
|
137
|
-
- Added `Layout.frame(...)` for nested layouts (`Layout` inside `Layout`).
|
|
138
|
-
- Added `Layout.cluster(...)` wrapping flow: widgets keep content/`width=` sizes and wrap left-to-right.
|
|
139
|
-
- Added `Layout.paired(left, right, ..., sync_yscroll=)` for side-by-side panes with optional text y-scroll sync.
|
|
140
|
-
- Docs/examples/tests for the above (`filepicker_demo`, `nested_frame_demo`, `cluster_demo`, `paired_demo`).
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## Snapshot (v0.4.4) — text wrap/h-scroll, public runtime API
|
|
145
|
-
|
|
146
|
-
### Text widget ergonomics (continued)
|
|
147
|
-
|
|
148
|
-
- Added `wrap=` option to `@app.text` for logical-line display control:
|
|
149
|
-
- `Wrap.WORD` (default) — wrap at word boundaries.
|
|
150
|
-
- `Wrap.NONE` — keep each logical line on one row (requires horizontal scrolling).
|
|
151
|
-
- `Wrap.CHAR` — wrap at any character boundary.
|
|
152
|
-
- Added a new `nextpytk.types.Wrap` typed-constant class and `WrapLike` union.
|
|
153
|
-
- Added `h_scroll=` option to `@app.text`: when True, a horizontal scrollbar is added (and wired to `xscrollcommand`) so long lines are reachable in `Wrap.NONE` mode. The horizontal scrollbar is tracked in `app._text_hscrollbars`.
|
|
154
|
-
|
|
155
|
-
### Decorator type-definition sync
|
|
156
|
-
|
|
157
|
-
- `TreeviewOptions.double_click` is now typed and honored by `@app.treeview`.
|
|
158
|
-
- Defaults to `True`: the `activate` handler fires on double-click (previous behavior).
|
|
159
|
-
- When `False`, `activate` fires on a single `ButtonRelease-1` instead.
|
|
160
|
-
- `_on_treeview_activate` accepts an optional `event` argument so the single-click binding can reuse it.
|
|
161
|
-
|
|
162
|
-
### Public runtime API
|
|
163
|
-
|
|
164
|
-
- Added `app.layout_frame(name)` — returns the layout frame that owns a widget (the section frame), so applications can place/re-parent widgets with grid/pack without reaching into the private `_widget_masters`.
|
|
165
|
-
- This is the public replacement for the internal `_widget_masters` access that previously forced applications to depend on private state.
|
|
166
|
-
|
|
167
|
-
### Per-widget design-token overrides
|
|
168
|
-
|
|
169
|
-
- All widget decorators now accept `widget_kwargs: dict[str, Any]` (on `CommonWidgetOptions`).
|
|
170
|
-
- Keys are widget-native tk/ttk options (`padx`, `pady`, `bg`, `fg`, `font`, …); values are the native values.
|
|
171
|
-
- Overrides are applied after construction by `_apply_widget_overrides()`. An invalid/unknown key raises no error: a `TclError` is swallowed so one bad key does not abort the build.
|
|
172
|
-
- This removes the need to monkey-patch the shared `tokens` module to tweak a single widget's appearance.
|
|
173
|
-
|
|
174
|
-
### Decorator argument validation
|
|
175
|
-
|
|
176
|
-
- Added `_validate_choice()` / `_validate_positive_int()` helpers used by the decorators to reject invalid enum-like and non-positive-int options at **registration time** (before any Tk widget is built).
|
|
177
|
-
- Validated options:
|
|
178
|
-
- `text`: `state` (`normal`/`disabled`/`active`), `wrap` (`word`/`none`/`char`)
|
|
179
|
-
- `scale`, `progressbar`, `paned`: `orient` (`horizontal`/`vertical`)
|
|
180
|
-
- `listbox`, `treeview`: `selectmode` (`single`/`browse`/`multiple`/`extended`)
|
|
181
|
-
- `filepicker`: `mode` (`open`/`open_multiple`/`save`/`directory`)
|
|
182
|
-
- `progressbar`: `mode` (`determinate`/`indeterminate`)
|
|
183
|
-
- A mistyped value (e.g. `wrap="wrap"`) now raises a clear `ValueError` naming the option, widget, and allowed values — instead of a cryptic `_tkinter.TclError` at runtime.
|
|
184
|
-
|
|
185
|
-
### Paired line-number gutters
|
|
186
|
-
|
|
187
|
-
- Added `line_numbers=True` to `Layout.paired(...)`.
|
|
188
|
-
- Each side gains a read-only line-number gutter (`disabled` tk.Text, `takefocus=0`, arrow cursor).
|
|
189
|
-
- **Reliable sync via a single shared vertical scrollbar**: both panes and both gutters drive one shared scrollbar. Each widget's `yscrollcommand` is chained so scrolling any widget moves the other three, and the shared scrollbar's `command` drives all four. This avoids the `yview_moveto` drift that plagued per-widget gutter chaining.
|
|
190
|
-
- Gutters stay in sync when content changes via `app.text_set()` (a new `app.on_text_set()` hook) and when the editable right pane is typed into (`<KeyRelease>`).
|
|
191
|
-
- Added `examples/paired_demo.py` demonstrating `line_numbers=True`.
|
|
192
|
-
- The new `app.on_text_set(name, hook)` public helper lets layout features react to programmatic text replacement.
|
|
193
|
-
|
|
194
|
-
### Dynamic layout switching (swap targets)
|
|
195
|
-
|
|
196
|
-
- Added `Layout.target(name)` to reserve a swap region whose contents change at runtime (mirrors HTMX `hx-target`).
|
|
197
|
-
- Added `@app.swap(name, variants=..., default=...)` to declare the layouts that can fill a target (mirrors HTMX `hx-swap`), and `app.swap_view(name, variant)` to switch imperatively.
|
|
198
|
-
- Variants are mounted up-front and shown/hidden via `pack`/`pack_forget`, so widget state (treeview selection, scroll position) survives switching. Surrounding `Layout.section` blocks (toolbar, status) stay fixed.
|
|
199
|
-
- Added `examples/swap_demo.py` (folder view <-> paired diff view).
|
|
200
|
-
|
|
201
|
-
### Tests
|
|
202
|
-
|
|
203
|
-
- Added `tests/test_widget_public_api.py` covering `layout_frame` and `widget_kwargs` (applied, ignored-invalid).
|
|
204
|
-
- Added `tests/test_decorator_validation.py` covering invalid-option rejection for every validated widget.
|
|
205
|
-
- Added wrap/h-scroll tests to `tests/test_text.py`.
|
|
206
|
-
- Added `double_click` tests to `tests/test_state.py`.
|
|
207
|
-
- Added paired line-number gutter tests to `tests/test_paired.py` (creation, population, shared-scrollbar sync).
|
|
208
|
-
- Added swap tests to `tests/test_swap.py` (variant registration, build, switching, back-and-forth, pre-build intent).
|
|
209
|
-
- Suite grew from 181 to 208 passing; pyright clean.
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
## Snapshot (v0.4.5) — Flutter-style layout vocabulary
|
|
214
|
-
|
|
215
|
-
### `cluster` → `wrap` rename
|
|
216
|
-
|
|
217
|
-
- `Layout.cluster()` renamed to `Layout.wrap()`, matching Flutter's `Wrap`
|
|
218
|
-
widget. `cluster()` is kept as a deprecated alias (emits a
|
|
219
|
-
`DeprecationWarning`) and will be removed in v0.5.0.
|
|
220
|
-
- `LayoutBuilder.cluster()` → `LayoutBuilder.wrap()` (same alias story).
|
|
221
|
-
- `examples/cluster_demo.py` → `examples/wrap_demo.py`; `tests/test_cluster.py`
|
|
222
|
-
→ `tests/test_wrap.py`.
|
|
223
|
-
- `gap` keyword deprecated in favor of explicit `gapx`/`gapy` (SPACE tokens).
|
|
224
|
-
|
|
225
|
-
### `Flex` — absorb leftover space (Flutter `Expanded` analog)
|
|
226
|
-
|
|
227
|
-
- Added `nextpytk.types.Flex(name, flex=...)`; a `wrap` child wrapped in `Flex`
|
|
228
|
-
absorbs a share of its row's leftover horizontal space proportional to its
|
|
229
|
-
flex factor, instead of keeping natural content width.
|
|
230
|
-
- `widget_names()` and `mount_frames_into` resolve `Flex`-wrapped names.
|
|
231
|
-
|
|
232
|
-
### `FlowDelegate` / `Layout.flow` — custom flow (Flutter `Flow` analog)
|
|
233
|
-
|
|
234
|
-
- Added `nextpytk.layout.Constraints` (width/height/min/max) and
|
|
235
|
-
`nextpytk.layout.FlowDelegate` (abstract `compute_positions` /
|
|
236
|
-
`compute_height`).
|
|
237
|
-
- `Layout.flow(*widgets, delegate=...)` and `LayoutBuilder.flow(...)` position
|
|
238
|
-
children via the delegate using `place`; recomputed on `<Configure>` resize.
|
|
239
|
-
- `_place_flow` / `_Flow` block dispatched in `mount_frames_into` /
|
|
240
|
-
`pack_children_for`.
|
|
241
|
-
- `examples/wrap_demo.py` demonstrates `wrap`, `wrap`+`Flex`, and a
|
|
242
|
-
`GridDelegate` flow.
|
|
243
|
-
|
|
244
|
-
### Theme / debug
|
|
245
|
-
|
|
246
|
-
- Button styles use `width=0` (cancel clam's `width=-11` min ~182px) so button
|
|
247
|
-
width tracks label text.
|
|
248
|
-
- `_check_radio_padding` unified to (16,12) to match buttons;
|
|
249
|
-
`indicatormargin=(0,0,SPACE[2],0)` adds 8px between indicator and label
|
|
250
|
-
(clam ignores `indicatorpadding`).
|
|
251
|
-
- `debug_layout()` detects geometry-manager conflicts;
|
|
252
|
-
`check_layout_conflicts()` warns on each.
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## Snapshot (v0.4.6) — section placement fix + layout ergonomics
|
|
257
|
-
|
|
258
|
-
### Bug fix: `section(..., side=...)` now controls the section frame placement
|
|
259
|
-
|
|
260
|
-
- `_pack_section_frame` hardcoded `side="top"`, ignoring the requested
|
|
261
|
-
placement side. A `side="bottom"` section declared after an `expand=True`
|
|
262
|
-
block (e.g. a status bar under a growing body) was pushed off-view and
|
|
263
|
-
collapsed to 1x1.
|
|
264
|
-
- Fixed by honoring `block.side` on the section frame pack call.
|
|
265
|
-
|
|
266
|
-
### Child packing decoupled from frame placement
|
|
267
|
-
|
|
268
|
-
- Added `_Row.child_side` to separate the section frame's placement side
|
|
269
|
-
(`side`) from the side used to pack children *inside* the frame. A
|
|
270
|
-
multi-widget section still lays its children out left-to-right (`"left"`)
|
|
271
|
-
regardless of where the frame itself is placed (`side="top"`/`"bottom"`/
|
|
272
|
-
`"left"`/`"right"`); a single-widget section packs its child with the
|
|
273
|
-
frame's own placement side.
|
|
274
|
-
|
|
275
|
-
### Tests / example
|
|
276
|
-
|
|
277
|
-
- Added `tests/test_layout_side.py` (regression coverage: frame side
|
|
278
|
-
honored, default `top` preserved, multi-widget child `"left"` + frame
|
|
279
|
-
`"top"`/`"bottom"`).
|
|
280
|
-
- Added `examples/bottom_bar_demo.py` demonstrating a `side="bottom"` bar
|
|
281
|
-
pinned below an expandable body.
|
|
282
|
-
|
|
283
|
-
### Known limitation: `side="bottom"` + focusable children
|
|
284
|
-
|
|
285
|
-
`side="bottom"` controls only the pack **parcel** (visual placement). Tk's
|
|
286
|
-
`tk_focusNext` / `tk_focusPrev` traverse the widget tree in *insertion order*
|
|
287
|
-
(`winfo children`), which is independent of the pack parcel. Because a
|
|
288
|
-
`side="bottom"` section must be declared early in the tree (so an
|
|
289
|
-
`expand=True` sibling doesn't push it off-view), any focusable child in it
|
|
290
|
-
(Entry, Button, …) lands **early** in the Tab order even though it is
|
|
291
|
-
visually at the bottom.
|
|
292
|
-
|
|
293
|
-
This is fine for non-focusable chrome (labels, a `status_bar`), but **not**
|
|
294
|
-
for bottom-docked interactive widgets: visual order (top → bottom controls)
|
|
295
|
-
and focus order (bottom controls first) diverge. The fix is to wire the
|
|
296
|
-
visual tab order explicitly (the same `_wire_tab_order` used by `wrap` /
|
|
297
|
-
`flow`, which intercepts `<Key-Tab>` / `<Shift-Key-Tab>` and moves focus in
|
|
298
|
-
visual row-major order). Planned to generalize for `side="bottom"` sections.
|
|
299
|
-
|
|
300
|
-
---
|
|
301
|
-
|
|
302
|
-
## Near term (post-0.4.5)
|
|
303
|
-
|
|
304
|
-
### A11y
|
|
305
|
-
|
|
306
|
-
- [ ] `@app.status` live-region behavior (ARIA `aria-live` equivalent / WCAG 4.1.3)
|
|
307
|
-
- [ ] Tk 9.1 `tk accessible set_acc_*` real-device verification (9.1b0 + NVDA)
|
|
308
|
-
- [ ] Role vocabulary mapping to Tk side
|
|
309
|
-
- [ ] Automatic `emit_selection_change` (leverage `apply_state` knowing which widgets changed)
|
|
310
|
-
|
|
311
|
-
### Type hints
|
|
312
|
-
|
|
313
|
-
- [ ] Further TypedDict / Protocol coverage for decorator arguments
|
|
314
|
-
- [ ] Fluent API equivalent to `_GridBuilder.columnconfigure`
|
|
315
|
-
|
|
316
|
-
### Widget expansion
|
|
317
|
-
|
|
318
|
-
- [x] Per-widget `bind` for entry and listbox (`events=` on `@app.entry` and `@app.listbox`)
|
|
319
|
-
- [x] Per-widget `bind` for other widgets via `widget_kwargs` / widget-level options
|
|
320
|
-
- [ ] Per-widget `bind` for other widgets (`text`, `combobox`, `treeview`, `button`, etc.)
|
|
321
|
-
- [x] listbox callback value design: keep selection index (unify with `treeview`)
|
|
322
|
-
- [x] `@app.filepicker` — `filedialog` wrapper (schema/tool naming still open)
|
|
323
|
-
- [x] Text `wrap=` / `h_scroll=` options (v0.4.4)
|
|
324
|
-
|
|
325
|
-
### Layout DSL
|
|
326
|
-
|
|
327
|
-
- [x] Deprecate index-order plural methods `col_weights`/`row_weights`/`col_minsizes`/`row_minsizes` in 0.4.1 (remove in 0.5.0).
|
|
328
|
-
- [ ] CSS-grid-inspired layout API for 0.5.0: explicit `grid-template-columns` / `grid-template-rows` strings (e.g. `"1fr 2fr"`, `"auto 1fr"`), area-based placement, and gap tokens. Reduce misuse by making rows/columns and widget placement visually aligned in one declaration.
|
|
329
|
-
- [x] Nested frames (`Layout` inside `Layout`) via `Layout.frame(...)`
|
|
330
|
-
- [x] Global default for `padx`/`pady` via `Layout(spacing=...)`
|
|
331
|
-
- [x] `Layout.cluster(...)` wrapping flow; `Layout.paired(..., sync_yscroll=)`
|
|
332
|
-
- [x] `Layout.cluster(...)` → `Layout.wrap(...)` rename with deprecated alias (v0.4.5)
|
|
333
|
-
- [x] `Flex` (Flutter `Expanded` analog) for `wrap` (v0.4.5)
|
|
334
|
-
- [x] `FlowDelegate` / `Layout.flow` custom flow (Flutter `Flow` analog) (v0.4.5)
|
|
335
|
-
- [x] `Layout.paired(..., line_numbers=True)` read-only line-number gutters (v0.4.4)
|
|
336
|
-
- [x] `Layout.target(...)` + `@app.swap(...)` dynamic region switching (v0.4.4)
|
|
337
|
-
|
|
338
|
-
### Agent / LLM integration
|
|
339
|
-
|
|
340
|
-
- [ ] Expose `schema()` as Function Calling definition (bind `sequence`, treeview column defs, etc.)
|
|
341
|
-
- [ ] `@agent_tool` integration (GUI operations as agent vocabulary)
|
|
342
|
-
- [ ] Surface `@app.filepicker` in `schema()` / tool vocabulary
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
|
-
### v0.5.0 themes / chrome decoupling
|
|
347
|
-
|
|
348
|
-
- Remove deprecated `theme=True/False` support; `theme` becomes `str` only.
|
|
349
|
-
- Remove deprecated `EventSeq.DOUBLE_CLICK`; use `EventSeq.PRIMARY_DOUBLE_CLICK` or `DOUBLE_BUTTON_1`.
|
|
350
|
-
- Decouple `Layout().header()` / `.status()` chrome from Kizashi-specific tokens so they render safely with any ttk theme (`"none"`, `"clam"`, etc.).
|
|
351
|
-
- Extract Kizashi as a standalone ttk theme package so it can be proposed upstream or reused independently of nextpytk.
|
|
352
|
-
|
|
353
|
-
### Longer term
|
|
354
|
-
|
|
355
|
-
### ttk Style layer
|
|
356
|
-
|
|
357
|
-
- [x] Theme switching (`ttk.Style().theme_use(...)`)
|
|
358
|
-
- [ ] `Layout.style("my_button", background=..., font=...)` style definitions
|
|
359
|
-
|
|
360
|
-
### Declarative components
|
|
361
|
-
|
|
362
|
-
- [ ] `@app.component` decorator (React-like reuse, replacing `Layout`)
|
|
363
|
-
- [ ] State type definitions and validation (no Pydantic)
|
|
364
|
-
|
|
365
|
-
### Testing
|
|
366
|
-
|
|
367
|
-
- [x] Headless execution tests (`tests/` — withdrawn root, build/apply_state/callback drive)
|
|
368
|
-
- [ ] WidgetSpec unit tests
|
|
369
|
-
|
|
370
|
-
---
|
|
371
|
-
|
|
372
|
-
## Earlier snapshots
|
|
373
|
-
|
|
374
|
-
### v0.3.0
|
|
375
|
-
|
|
376
|
-
Builder registry, headless tests, a11y wiring (`_apply_a11y`), error policy, bugfixes.
|
|
377
|
-
`run_async` / `spawn` / `@app.job`, public runtime API (`build_widgets`, `apply_state`, `register_widget_builder`, …).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|