nextpytk 0.4.7__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.7 → nextpytk-0.4.8}/PKG-INFO +6 -1
- {nextpytk-0.4.7 → nextpytk-0.4.8}/README.md +5 -0
- nextpytk-0.4.8/ROADMAP.md +61 -0
- {nextpytk-0.4.7 → nextpytk-0.4.8}/pyproject.toml +1 -1
- {nextpytk-0.4.7 → nextpytk-0.4.8}/src/nextpytk/app.py +111 -19
- {nextpytk-0.4.7 → nextpytk-0.4.8}/src/nextpytk/theme.py +56 -26
- {nextpytk-0.4.7 → nextpytk-0.4.8}/src/nextpytk/tokens.py +1 -1
- nextpytk-0.4.7/ROADMAP.md +0 -426
- {nextpytk-0.4.7 → nextpytk-0.4.8}/.gitignore +0 -0
- {nextpytk-0.4.7 → nextpytk-0.4.8}/LICENSE +0 -0
- {nextpytk-0.4.7 → nextpytk-0.4.8}/src/nextpytk/__init__.py +0 -0
- {nextpytk-0.4.7 → nextpytk-0.4.8}/src/nextpytk/layout.py +0 -0
- {nextpytk-0.4.7 → nextpytk-0.4.8}/src/nextpytk/types.py +0 -0
- {nextpytk-0.4.7 → 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
|
|
@@ -858,6 +858,7 @@ uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async
|
|
|
858
858
|
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
859
859
|
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
860
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)
|
|
861
862
|
```
|
|
862
863
|
|
|
863
864
|
---
|
|
@@ -880,6 +881,10 @@ uv run python examples/bottom_bar_demo.py # pinned bottom bar (section sid
|
|
|
880
881
|
- **`CustomTkinter`**: Modern look via Canvas rendering. nextpytk takes the opposite approach: use native widgets and embed A11y from the start.
|
|
881
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.
|
|
882
883
|
|
|
884
|
+
## Changelog
|
|
885
|
+
|
|
886
|
+
See [CHANGES.md](./CHANGES.md) for the full release history.
|
|
887
|
+
|
|
883
888
|
## License
|
|
884
889
|
|
|
885
890
|
MIT
|
|
@@ -834,6 +834,7 @@ uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async
|
|
|
834
834
|
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
835
835
|
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
836
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)
|
|
837
838
|
```
|
|
838
839
|
|
|
839
840
|
---
|
|
@@ -856,6 +857,10 @@ uv run python examples/bottom_bar_demo.py # pinned bottom bar (section sid
|
|
|
856
857
|
- **`CustomTkinter`**: Modern look via Canvas rendering. nextpytk takes the opposite approach: use native widgets and embed A11y from the start.
|
|
857
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.
|
|
858
859
|
|
|
860
|
+
## Changelog
|
|
861
|
+
|
|
862
|
+
See [CHANGES.md](./CHANGES.md) for the full release history.
|
|
863
|
+
|
|
859
864
|
## License
|
|
860
865
|
|
|
861
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]] = []
|
|
@@ -358,6 +373,9 @@ class TkApp:
|
|
|
358
373
|
self._text_scroll_sync.clear()
|
|
359
374
|
self._text_set_hooks.clear()
|
|
360
375
|
self._a11y_last_toggle.clear()
|
|
376
|
+
self._pending_ingest.clear()
|
|
377
|
+
self._ingest_flush_job = None
|
|
378
|
+
self._syncing_var_keys.clear()
|
|
361
379
|
self._current_view = None
|
|
362
380
|
self._current_stage = None
|
|
363
381
|
self._swap_targets.clear()
|
|
@@ -1290,7 +1308,7 @@ class TkApp:
|
|
|
1290
1308
|
layout, row_jobs, grid_jobs = jobs_by_view[view]
|
|
1291
1309
|
layout.pack_children_for(self, row_jobs, grid_jobs)
|
|
1292
1310
|
else:
|
|
1293
|
-
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])
|
|
1294
1312
|
|
|
1295
1313
|
def _on_tab_changed(_event: tk.Event[tk.Misc] | None = None) -> None:
|
|
1296
1314
|
current = nb.select()
|
|
@@ -1462,7 +1480,7 @@ class TkApp:
|
|
|
1462
1480
|
layout, row_jobs, grid_jobs = jobs_by_stage[stage]
|
|
1463
1481
|
layout.pack_children_for(self, row_jobs, grid_jobs)
|
|
1464
1482
|
else:
|
|
1465
|
-
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])
|
|
1466
1484
|
|
|
1467
1485
|
active = str(self._state.get(key, stages[0] if stages else ""))
|
|
1468
1486
|
if active not in stages:
|
|
@@ -2609,7 +2627,7 @@ class TkApp:
|
|
|
2609
2627
|
"""
|
|
2610
2628
|
width = options.get("width", 300)
|
|
2611
2629
|
height = options.get("height", 200)
|
|
2612
|
-
bg = options.get("bg",
|
|
2630
|
+
bg = options.get("bg", t.SURFACE)
|
|
2613
2631
|
description = options.get("description")
|
|
2614
2632
|
items = options.get("items")
|
|
2615
2633
|
takefocus = options.get("takefocus")
|
|
@@ -2658,15 +2676,21 @@ class TkApp:
|
|
|
2658
2676
|
update_to_apply.pop(stage_key, None)
|
|
2659
2677
|
|
|
2660
2678
|
self._state.update(update_to_apply)
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
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)
|
|
2670
2694
|
if full:
|
|
2671
2695
|
self._sync_widgets()
|
|
2672
2696
|
else:
|
|
@@ -2691,6 +2715,61 @@ class TkApp:
|
|
|
2691
2715
|
# Keep state in sync with the rendered stage.
|
|
2692
2716
|
self._state[key] = new_stage
|
|
2693
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
|
+
|
|
2694
2773
|
def _known_state_keys(self) -> set[str]:
|
|
2695
2774
|
"""Return the set of state keys that have meaning in the app."""
|
|
2696
2775
|
keys: set[str] = set()
|
|
@@ -3346,6 +3425,19 @@ class TkApp:
|
|
|
3346
3425
|
|
|
3347
3426
|
self._first_focusable: tk.Widget | None = None
|
|
3348
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
|
+
|
|
3349
3441
|
for spec in self._widgets:
|
|
3350
3442
|
if spec.kind == "paned":
|
|
3351
3443
|
master = self._widget_masters.get(spec.name, self._root)
|
|
@@ -3746,7 +3838,7 @@ class TkApp:
|
|
|
3746
3838
|
)
|
|
3747
3839
|
w = ttk.Entry(master, textvariable=var, style=style)
|
|
3748
3840
|
self._tk_widgets[spec.name] = w
|
|
3749
|
-
self.
|
|
3841
|
+
self._register_var(spec.name, var)
|
|
3750
3842
|
for opt in ("show", "width", "state"):
|
|
3751
3843
|
if opt in e:
|
|
3752
3844
|
w.configure(**{opt: e[opt]})
|
|
@@ -3792,7 +3884,7 @@ class TkApp:
|
|
|
3792
3884
|
# anchor is a ttk layout option (style.layout), not a constructor kwarg
|
|
3793
3885
|
# for ttk.Checkbutton; left alignment is enforced by the Kizashi style.
|
|
3794
3886
|
self._tk_widgets[spec.name] = w
|
|
3795
|
-
self.
|
|
3887
|
+
self._register_var(key, var)
|
|
3796
3888
|
if spec.on_update is not None:
|
|
3797
3889
|
fn = spec.on_update
|
|
3798
3890
|
w.configure(command=lambda s=spec, f=fn, v=var, k=key:
|
|
@@ -3803,7 +3895,7 @@ class TkApp:
|
|
|
3803
3895
|
gk = e.get("group_key", "radio")
|
|
3804
3896
|
val = e.get("rb_value", "")
|
|
3805
3897
|
if gk not in self._tk_vars:
|
|
3806
|
-
self.
|
|
3898
|
+
self._register_var(gk, tk.StringVar(value=""))
|
|
3807
3899
|
var = self._tk_vars[gk]
|
|
3808
3900
|
# padding is owned by the theme style (44px min target size)
|
|
3809
3901
|
style = "TRadiobutton"
|
|
@@ -3933,7 +4025,7 @@ class TkApp:
|
|
|
3933
4025
|
length=e.get("length", 200 if orient_str == "horizontal" else 100),
|
|
3934
4026
|
) # type: ignore[arg-type]
|
|
3935
4027
|
self._tk_widgets[spec.name] = w
|
|
3936
|
-
self.
|
|
4028
|
+
self._register_var(key, var)
|
|
3937
4029
|
self._state[key] = str(var.get())
|
|
3938
4030
|
if spec.on_update is not None:
|
|
3939
4031
|
fn = spec.on_update
|
|
@@ -3964,7 +4056,7 @@ class TkApp:
|
|
|
3964
4056
|
kwargs["font"] = e["font"]
|
|
3965
4057
|
w = ttk.Spinbox(master, textvariable=var, **kwargs)
|
|
3966
4058
|
self._tk_widgets[spec.name] = w
|
|
3967
|
-
self.
|
|
4059
|
+
self._register_var(key, var)
|
|
3968
4060
|
if init_val:
|
|
3969
4061
|
self._state[key] = init_val
|
|
3970
4062
|
if spec.on_update is not None:
|
|
@@ -3990,7 +4082,7 @@ class TkApp:
|
|
|
3990
4082
|
kwargs["font"] = e["font"]
|
|
3991
4083
|
w = ttk.Combobox(master, **kwargs)
|
|
3992
4084
|
self._tk_widgets[spec.name] = w
|
|
3993
|
-
self.
|
|
4085
|
+
self._register_var(key, var)
|
|
3994
4086
|
if init_val:
|
|
3995
4087
|
self._state[key] = init_val
|
|
3996
4088
|
if spec.on_update is not None:
|
|
@@ -4001,7 +4093,7 @@ class TkApp:
|
|
|
4001
4093
|
def _build_listbox(self, spec: WidgetSpec, master: tk.Misc) -> None:
|
|
4002
4094
|
e = spec.extras
|
|
4003
4095
|
kwargs_lb: dict[str, Any] = {
|
|
4004
|
-
"bg": t.
|
|
4096
|
+
"bg": t.BG,
|
|
4005
4097
|
"fg": t.TEXT,
|
|
4006
4098
|
"selectbackground": t.ACCENT_RAMP[200],
|
|
4007
4099
|
"selectforeground": t.ACCENT_RAMP[700],
|
|
@@ -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.7/ROADMAP.md
DELETED
|
@@ -1,426 +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
|
-
## Snapshot (v0.4.7) — paired gutter logical-line fix
|
|
303
|
-
|
|
304
|
-
### Bug fix: line-number gutters count logical lines, not display lines
|
|
305
|
-
|
|
306
|
-
- `Layout.paired(..., line_numbers=True)` gutters numbered **physical**
|
|
307
|
-
(wrapped) rows: `_reconcile_gutter` / `_populate_gutters` used
|
|
308
|
-
`text.index("end-1c")`, which counts display rows. With `wrap="word"` /
|
|
309
|
-
`"char"`, a long line that wrapped inflated the gutter count beyond the
|
|
310
|
-
real line count.
|
|
311
|
-
- Added `_logical_line_count(text)`, which counts newline characters (a
|
|
312
|
-
wrapped row adds no newline), adjusted for whether the buffer ends in a
|
|
313
|
-
newline. The gutter now mirrors true logical lines regardless of wrapping.
|
|
314
|
-
|
|
315
|
-
### Bug fix: re-entry recursion in gutter sync
|
|
316
|
-
|
|
317
|
-
- Rewriting a gutter calls `update_idletasks()`, which fires the shared
|
|
318
|
-
scrollbar's `yscrollcommand` and re-enters `_reconcile_gutter` /
|
|
319
|
-
`_populate_gutters` through `_chain_yview` — an infinite recursion that
|
|
320
|
-
surfaced once the gutter logic changed.
|
|
321
|
-
- Added a per-gutter `_syncing_gutter` re-entry guard to both helpers.
|
|
322
|
-
|
|
323
|
-
### Bug fix: `on_text_set` hook leak across re-runs
|
|
324
|
-
|
|
325
|
-
- `app.on_text_set()` hooks were never cleared by `clear_runtime()`, so
|
|
326
|
-
hooks (e.g. gutter line-number sync) accumulated across re-runs — such as
|
|
327
|
-
swap variants rebuilt at runtime — causing the gutter sync to run multiple
|
|
328
|
-
times per `text_set`.
|
|
329
|
-
- `clear_runtime()` now also clears `_text_set_hooks`.
|
|
330
|
-
|
|
331
|
-
### Tests
|
|
332
|
-
|
|
333
|
-
- Added `test_paired_gutters_count_logical_lines_when_wrapped` (a long,
|
|
334
|
-
wrapping line must not inflate the gutter line numbers).
|
|
335
|
-
- Added `test_paired_gutter_sync_does_not_recursively_loop` (multiple
|
|
336
|
-
`text_set` calls do not recurse infinitely through the shared scrollbar).
|
|
337
|
-
- Added `test_clear_runtime_clears_text_set_hooks` (hooks are dropped on
|
|
338
|
-
`clear_runtime()`).
|
|
339
|
-
- `tests/test_paired.py` grew; full suite green.
|
|
340
|
-
|
|
341
|
-
### Docs: `line_numbers=True` takes precedence over `sync_yscroll`
|
|
342
|
-
|
|
343
|
-
- Documented that enabling `line_numbers` installs a shared scrollbar that
|
|
344
|
-
always keeps both panes (and both gutters) in lock-step, so
|
|
345
|
-
`sync_yscroll=False` is ignored when `line_numbers=True`. Callers needing
|
|
346
|
-
independent pane scrolling must leave `line_numbers` off. Clarified in the
|
|
347
|
-
`Layout.paired` docstring and README (EN / JA).
|
|
348
|
-
|
|
349
|
-
---
|
|
350
|
-
|
|
351
|
-
## Near term (post-0.4.7)
|
|
352
|
-
|
|
353
|
-
### A11y
|
|
354
|
-
|
|
355
|
-
- [ ] `@app.status` live-region behavior (ARIA `aria-live` equivalent / WCAG 4.1.3)
|
|
356
|
-
- [ ] Tk 9.1 `tk accessible set_acc_*` real-device verification (9.1b0 + NVDA)
|
|
357
|
-
- [ ] Role vocabulary mapping to Tk side
|
|
358
|
-
- [ ] Automatic `emit_selection_change` (leverage `apply_state` knowing which widgets changed)
|
|
359
|
-
|
|
360
|
-
### Type hints
|
|
361
|
-
|
|
362
|
-
- [ ] Further TypedDict / Protocol coverage for decorator arguments
|
|
363
|
-
- [ ] Fluent API equivalent to `_GridBuilder.columnconfigure`
|
|
364
|
-
|
|
365
|
-
### Widget expansion
|
|
366
|
-
|
|
367
|
-
- [x] Per-widget `bind` for entry and listbox (`events=` on `@app.entry` and `@app.listbox`)
|
|
368
|
-
- [x] Per-widget `bind` for other widgets via `widget_kwargs` / widget-level options
|
|
369
|
-
- [ ] Per-widget `bind` for other widgets (`text`, `combobox`, `treeview`, `button`, etc.)
|
|
370
|
-
- [x] listbox callback value design: keep selection index (unify with `treeview`)
|
|
371
|
-
- [x] `@app.filepicker` — `filedialog` wrapper (schema/tool naming still open)
|
|
372
|
-
- [x] Text `wrap=` / `h_scroll=` options (v0.4.4)
|
|
373
|
-
|
|
374
|
-
### Layout DSL
|
|
375
|
-
|
|
376
|
-
- [x] Deprecate index-order plural methods `col_weights`/`row_weights`/`col_minsizes`/`row_minsizes` in 0.4.1 (remove in 0.5.0).
|
|
377
|
-
- [ ] 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.
|
|
378
|
-
- [x] Nested frames (`Layout` inside `Layout`) via `Layout.frame(...)`
|
|
379
|
-
- [x] Global default for `padx`/`pady` via `Layout(spacing=...)`
|
|
380
|
-
- [x] `Layout.cluster(...)` wrapping flow; `Layout.paired(..., sync_yscroll=)`
|
|
381
|
-
- [x] `Layout.cluster(...)` → `Layout.wrap(...)` rename with deprecated alias (v0.4.5)
|
|
382
|
-
- [x] `Flex` (Flutter `Expanded` analog) for `wrap` (v0.4.5)
|
|
383
|
-
- [x] `FlowDelegate` / `Layout.flow` custom flow (Flutter `Flow` analog) (v0.4.5)
|
|
384
|
-
- [x] `Layout.paired(..., line_numbers=True)` read-only line-number gutters (v0.4.4)
|
|
385
|
-
- [x] `Layout.target(...)` + `@app.swap(...)` dynamic region switching (v0.4.4)
|
|
386
|
-
|
|
387
|
-
### Agent / LLM integration
|
|
388
|
-
|
|
389
|
-
- [ ] Expose `schema()` as Function Calling definition (bind `sequence`, treeview column defs, etc.)
|
|
390
|
-
- [ ] `@agent_tool` integration (GUI operations as agent vocabulary)
|
|
391
|
-
- [ ] Surface `@app.filepicker` in `schema()` / tool vocabulary
|
|
392
|
-
|
|
393
|
-
---
|
|
394
|
-
|
|
395
|
-
### v0.5.0 themes / chrome decoupling
|
|
396
|
-
|
|
397
|
-
- Remove deprecated `theme=True/False` support; `theme` becomes `str` only.
|
|
398
|
-
- Remove deprecated `EventSeq.DOUBLE_CLICK`; use `EventSeq.PRIMARY_DOUBLE_CLICK` or `DOUBLE_BUTTON_1`.
|
|
399
|
-
- Decouple `Layout().header()` / `.status()` chrome from Kizashi-specific tokens so they render safely with any ttk theme (`"none"`, `"clam"`, etc.).
|
|
400
|
-
- Extract Kizashi as a standalone ttk theme package so it can be proposed upstream or reused independently of nextpytk.
|
|
401
|
-
|
|
402
|
-
### Longer term
|
|
403
|
-
|
|
404
|
-
### ttk Style layer
|
|
405
|
-
|
|
406
|
-
- [x] Theme switching (`ttk.Style().theme_use(...)`)
|
|
407
|
-
- [ ] `Layout.style("my_button", background=..., font=...)` style definitions
|
|
408
|
-
|
|
409
|
-
### Declarative components
|
|
410
|
-
|
|
411
|
-
- [ ] `@app.component` decorator (React-like reuse, replacing `Layout`)
|
|
412
|
-
- [ ] State type definitions and validation (no Pydantic)
|
|
413
|
-
|
|
414
|
-
### Testing
|
|
415
|
-
|
|
416
|
-
- [x] Headless execution tests (`tests/` — withdrawn root, build/apply_state/callback drive)
|
|
417
|
-
- [ ] WidgetSpec unit tests
|
|
418
|
-
|
|
419
|
-
---
|
|
420
|
-
|
|
421
|
-
## Earlier snapshots
|
|
422
|
-
|
|
423
|
-
### v0.3.0
|
|
424
|
-
|
|
425
|
-
Builder registry, headless tests, a11y wiring (`_apply_a11y`), error policy, bugfixes.
|
|
426
|
-
`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
|
|
File without changes
|