nextpytk 0.4.3__tar.gz → 0.4.5__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.3 → nextpytk-0.4.5}/PKG-INFO +125 -14
- {nextpytk-0.4.3 → nextpytk-0.4.5}/README.md +124 -13
- {nextpytk-0.4.3 → nextpytk-0.4.5}/ROADMAP.md +120 -1
- {nextpytk-0.4.3 → nextpytk-0.4.5}/pyproject.toml +1 -1
- {nextpytk-0.4.3 → nextpytk-0.4.5}/src/nextpytk/__init__.py +10 -1
- {nextpytk-0.4.3 → nextpytk-0.4.5}/src/nextpytk/app.py +478 -29
- {nextpytk-0.4.3 → nextpytk-0.4.5}/src/nextpytk/layout.py +842 -86
- {nextpytk-0.4.3 → nextpytk-0.4.5}/src/nextpytk/theme.py +24 -6
- {nextpytk-0.4.3 → nextpytk-0.4.5}/src/nextpytk/types.py +55 -0
- {nextpytk-0.4.3 → nextpytk-0.4.5}/.gitignore +0 -0
- {nextpytk-0.4.3 → nextpytk-0.4.5}/LICENSE +0 -0
- {nextpytk-0.4.3 → nextpytk-0.4.5}/src/nextpytk/tokens.py +0 -0
- {nextpytk-0.4.3 → nextpytk-0.4.5}/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.5
|
|
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
|
|
@@ -212,6 +212,7 @@ Layout().section("msg", side=Side.LEFT, fill=Fill.X)
|
|
|
212
212
|
| `Orient` | `Orient.HORIZONTAL/VERTICAL` | scale / paned orientation |
|
|
213
213
|
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/...` | border style |
|
|
214
214
|
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
215
|
+
| `Wrap` | `Wrap.WORD/NONE/CHAR` | text wrap mode |
|
|
215
216
|
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
216
217
|
| `EventSeq` | `EventSeq.RETURN/ESCAPE/BACKSPACE/DELETE/TAB/...` | event sequences for bindings |
|
|
217
218
|
| `EventSeq` (mouse) | `EventSeq.BUTTON_1/2/3`, `DOUBLE_BUTTON_1/2/3`, `PRIMARY_DOUBLE_CLICK` | mouse event sequences |
|
|
@@ -480,6 +481,7 @@ Options:
|
|
|
480
481
|
| `fill` | `"x"` | Frame `fill` passed to `pack` |
|
|
481
482
|
| `expand` | `False` | Frame `expand` passed to `pack` |
|
|
482
483
|
| `sync_yscroll` | `False` | Wire the two widgets' y-scroll commands together |
|
|
484
|
+
| `line_numbers` | `False` | Add read-only line-number gutters to both sides |
|
|
483
485
|
| `side`, `padx`, `pady`, `anchor` | — | Standard `Layout` block placement options |
|
|
484
486
|
|
|
485
487
|
When `sync_yscroll=True` and both widgets are `@app.text` widgets, scrolling
|
|
@@ -488,14 +490,21 @@ achieved per-widget with `@app.text(..., sync_yscroll_with="other")`; paired
|
|
|
488
490
|
layout provides a layout-level switch that works without editing widget
|
|
489
491
|
declarations.
|
|
490
492
|
|
|
491
|
-
|
|
493
|
+
With `line_numbers=True` each side gains a read-only line-number gutter. Both
|
|
494
|
+
panes and both gutters share a **single vertical scrollbar**, so the gutters
|
|
495
|
+
stay in lock-step with the content (no drift). Logical line numbers
|
|
496
|
+
(`1..n`) follow the pane content; the right pane's numbers update as you edit.
|
|
492
497
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
`
|
|
498
|
+
|
|
499
|
+
### Wrap layout
|
|
500
|
+
|
|
501
|
+
`Layout.wrap()` is a wrapping flow (Flutter `Wrap` analog): widgets are not
|
|
502
|
+
stretched to fill a column; each keeps the width implied by its content or
|
|
503
|
+
`width=` and they lay out left to right, wrapping to the next row whenever the
|
|
504
|
+
next widget no longer fits in the remaining frame width. Widgets in the same
|
|
505
|
+
row are vertically centered on their midline, so a tall `entry` aligns cleanly
|
|
506
|
+
with a shorter `button` or `checkbutton`. The gap inherits the layout spacing
|
|
507
|
+
by default (`gapx`/`gapy` override it independently).
|
|
499
508
|
|
|
500
509
|
```python
|
|
501
510
|
TAGS = ["python", "tkinter", "async", "uv", "type hints"]
|
|
@@ -504,13 +513,76 @@ for tag in TAGS:
|
|
|
504
513
|
def on_tag(values, tag=tag):
|
|
505
514
|
return {"msg": f"Selected: {tag}"}
|
|
506
515
|
|
|
507
|
-
app.run(layout=Layout(spacing=2).status("msg").
|
|
516
|
+
app.run(layout=Layout(spacing=2).status("msg").wrap(*TAGS))
|
|
508
517
|
```
|
|
509
518
|
|
|
510
|
-
|
|
511
|
-
rows automatically when the window is resized. Pass `
|
|
512
|
-
spacing default, and `side`/`fill`/`expand` to control how the
|
|
513
|
-
packed.
|
|
519
|
+
Wrap is ideal for tag clouds, toolbars, and filter UIs. The layout recomputes
|
|
520
|
+
rows automatically when the window is resized. Pass `gapx`/`gapy` to override
|
|
521
|
+
the spacing default, and `side`/`fill`/`expand` to control how the wrap frame
|
|
522
|
+
is packed.
|
|
523
|
+
|
|
524
|
+
Any wrap child wrapped in `Flex(name, flex=...)` absorbs leftover horizontal
|
|
525
|
+
space in its row (Flutter `Expanded` analog):
|
|
526
|
+
|
|
527
|
+
```python
|
|
528
|
+
from nextpytk import Flex
|
|
529
|
+
|
|
530
|
+
app.run(layout=Layout().wrap("filter", Flex("search", flex=2), "ok", gapx=2))
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
For custom positioning, `Layout.flow()` takes a `FlowDelegate` (Flutter `Flow`
|
|
534
|
+
analog) that computes each child's `(x, y, width, height)` from the available
|
|
535
|
+
`Constraints`. See `examples/wrap_demo.py` for both `Flex` and `Flow` usage.
|
|
536
|
+
|
|
537
|
+
**How it works.** `wrap` and `flow` position children with `place` (absolute
|
|
538
|
+
x/y) because `pack` cannot reflow onto new rows — with `pack -side left` an
|
|
539
|
+
overflowing child simply falls off the edge. `place` therefore implies a few
|
|
540
|
+
constraints:
|
|
541
|
+
|
|
542
|
+
- All children share one parent frame and are `place`-managed; they cannot
|
|
543
|
+
also be `pack`/`grid`-managed on the same master (mixing geometry managers
|
|
544
|
+
raises `TclError: conflicting geometry managers`).
|
|
545
|
+
- Widths come from `winfo_reqwidth()`; the frame uses `pack_propagate(False)`
|
|
546
|
+
and its height is explicit.
|
|
547
|
+
- On resize the flow is recomputed so rows re-wrap to the new width.
|
|
548
|
+
|
|
549
|
+
Because `place` bypasses Tk's native Tab traversal (which follows pack/grid
|
|
550
|
+
insertion order), `wrap` intercepts `<Tab>`/`<Shift-Tab>` and moves focus in
|
|
551
|
+
the visual row-major order instead. `Entry`/`Text` children are skipped (Tab
|
|
552
|
+
stays for text input); focus wraps between the first and last wrap item.
|
|
553
|
+
|
|
554
|
+
> **Note:** `Layout.cluster()` was renamed to `Layout.wrap()` in 0.5.0.
|
|
555
|
+
> `cluster()` remains as a deprecated alias and will be removed in 0.5.0.
|
|
556
|
+
|
|
557
|
+
### Dynamic region switching (swap targets)
|
|
558
|
+
|
|
559
|
+
`Layout.target()` reserves a region whose contents change at runtime, mirroring
|
|
560
|
+
HTMX `hx-target` / `hx-swap`. Surrounding sections (toolbar, status) stay fixed
|
|
561
|
+
while only the target region swaps:
|
|
562
|
+
|
|
563
|
+
```python
|
|
564
|
+
layout = Layout().wrap("go_dir", "go_file", "info").target("main_area")
|
|
565
|
+
|
|
566
|
+
@app.swap(
|
|
567
|
+
"main_area",
|
|
568
|
+
variants={
|
|
569
|
+
"dir": [Layout().section("dir_tree")],
|
|
570
|
+
"file": [Layout().paired("left_text", "right_text",
|
|
571
|
+
fill=Fill.BOTH, expand=True)],
|
|
572
|
+
},
|
|
573
|
+
default="dir",
|
|
574
|
+
)
|
|
575
|
+
def main_area():
|
|
576
|
+
pass
|
|
577
|
+
|
|
578
|
+
# Switch imperatively at runtime:
|
|
579
|
+
app.swap_view("main_area", "file")
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
`@app.swap` variants are mounted up-front and shown/hidden via pack, so widget
|
|
583
|
+
state (e.g. a treeview selection or scroll position) survives switching. Each
|
|
584
|
+
variant is a `Layout` or a list of widget names. See
|
|
585
|
+
`examples/swap_demo.py`.
|
|
514
586
|
|
|
515
587
|
### Fluent DSL
|
|
516
588
|
|
|
@@ -600,7 +672,7 @@ app.run(layout=b.build())
|
|
|
600
672
|
| `@app.combobox(name, values=..., values_key=..., readonly=..., font=...)` | ttk.Combobox | selected value `str` | `dict` |
|
|
601
673
|
| `@app.menubar(name)` | tk.Menu (window menubar) | — | menu item list |
|
|
602
674
|
| `@app.filepicker(name, mode=..., label=..., title=..., filetypes=..., ...)` | ttk.Button → tkinter.filedialog | selected path(s) `str`, `list[str]`, or `None` | `dict` |
|
|
603
|
-
| `@app.text(name, width=..., height=..., font=...)` | tk.Text | full content `str` | `dict` |
|
|
675
|
+
| `@app.text(name, width=..., height=..., font=..., wrap=..., h_scroll=...)` | tk.Text | full content `str` | `dict` |
|
|
604
676
|
| `@app.scale(name, from_=..., to=..., orient=...)` | ttk.Scale | value `str` | `dict` |
|
|
605
677
|
| `@app.spinbox(name, from_=..., to=..., values=..., font=...)` | ttk.Spinbox | value `str` | `dict` |
|
|
606
678
|
| `@app.listbox(name, items=..., items_key=..., selectmode=..., font=..., events=...)` | tk.Listbox | selected index `int` (`-1` if none) | `dict` |
|
|
@@ -651,6 +723,32 @@ Entry options: `placeholder`, `show`, `font`, `padding`, `width`, `state`.
|
|
|
651
723
|
Button, checkbutton, radiobutton, spinbox, combobox, listbox, text options:
|
|
652
724
|
- `font` is accepted uniformly. Under the hood, ttk widgets use a derived style so the rest of the theme (colors, maps, layout) is inherited.
|
|
653
725
|
|
|
726
|
+
Text options (continued):
|
|
727
|
+
- `wrap`: `Wrap.WORD` (default) / `Wrap.NONE` / `Wrap.CHAR`. `Wrap.NONE` keeps each logical line on a single row.
|
|
728
|
+
- `h_scroll`: when `True`, a horizontal scrollbar is added (and wired to `xscrollcommand`) so long lines are reachable, typically used with `wrap=Wrap.NONE`.
|
|
729
|
+
|
|
730
|
+
Every widget decorator also accepts `widget_kwargs: dict` for per-widget
|
|
731
|
+
design-token overrides applied after construction. Keys are widget-native
|
|
732
|
+
tk/ttk options (`padx`, `pady`, `bg`, `fg`, `font`, …). An invalid/unknown
|
|
733
|
+
key is ignored rather than aborting the build:
|
|
734
|
+
|
|
735
|
+
```python
|
|
736
|
+
@app.text("log", wrap="none", h_scroll=True,
|
|
737
|
+
widget_kwargs={"bg": "#1e1e1e", "fg": "#dcdcdc"})
|
|
738
|
+
def log(value): return {}
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Runtime access: `app.text_widget(name)` returns the real `tk.Text`;
|
|
742
|
+
`app.layout_frame(name)` returns the layout section frame that owns a
|
|
743
|
+
widget (for manual grid/pack placement or re-parenting).
|
|
744
|
+
`app.on_text_set(name, hook)` registers a callback to run after a text
|
|
745
|
+
widget's content is replaced (used by paired line-number gutters).
|
|
746
|
+
|
|
747
|
+
Enum-like options (`wrap`, `state`, `orient`, `selectmode`, `mode`) are
|
|
748
|
+
validated at registration time: an invalid value raises a clear `ValueError`
|
|
749
|
+
naming the option and the allowed values, instead of failing later with a
|
|
750
|
+
`TclError` when the widget is built.
|
|
751
|
+
|
|
654
752
|
`@app.message` creates an auto-wrapping label. `width` sets initial pixel width; `auto_width=True` (default) tracks parent container resize.
|
|
655
753
|
|
|
656
754
|
---
|
|
@@ -710,6 +808,18 @@ print(app.debug_layout())
|
|
|
710
808
|
# → {"title": "...", "sections": [{"widgets": [{"name": "msg", "geometry": ..., ...}, ...]}]}
|
|
711
809
|
```
|
|
712
810
|
|
|
811
|
+
**Detecting geometry-manager conflicts.** Mixing `pack`/`grid`/`place` on one
|
|
812
|
+
master (e.g. manually `pack`/`grid`-ing into a `place`-managed `wrap`/`flow`
|
|
813
|
+
frame) cannot be fully prevented by the DSL, so `app.check_layout_conflicts()`
|
|
814
|
+
inspects the widget tree and reports any such mix without waiting for a
|
|
815
|
+
`TclError: conflicting geometry managers`:
|
|
816
|
+
|
|
817
|
+
```python
|
|
818
|
+
print(app.check_layout_conflicts())
|
|
819
|
+
# → [{"master_class": "Frame", "managers": ["pack", "place"], ...}] on conflict
|
|
820
|
+
# (a warning is also emitted for each conflict)
|
|
821
|
+
```
|
|
822
|
+
|
|
713
823
|
---
|
|
714
824
|
|
|
715
825
|
## Async-Native (asyncio + Tkinter)
|
|
@@ -741,6 +851,7 @@ uv run python examples/menubar_demo.py # menubar
|
|
|
741
851
|
uv run python examples/filepicker_demo.py # file picker
|
|
742
852
|
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
743
853
|
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
854
|
+
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
744
855
|
```
|
|
745
856
|
|
|
746
857
|
---
|
|
@@ -188,6 +188,7 @@ Layout().section("msg", side=Side.LEFT, fill=Fill.X)
|
|
|
188
188
|
| `Orient` | `Orient.HORIZONTAL/VERTICAL` | scale / paned orientation |
|
|
189
189
|
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/...` | border style |
|
|
190
190
|
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
191
|
+
| `Wrap` | `Wrap.WORD/NONE/CHAR` | text wrap mode |
|
|
191
192
|
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
192
193
|
| `EventSeq` | `EventSeq.RETURN/ESCAPE/BACKSPACE/DELETE/TAB/...` | event sequences for bindings |
|
|
193
194
|
| `EventSeq` (mouse) | `EventSeq.BUTTON_1/2/3`, `DOUBLE_BUTTON_1/2/3`, `PRIMARY_DOUBLE_CLICK` | mouse event sequences |
|
|
@@ -456,6 +457,7 @@ Options:
|
|
|
456
457
|
| `fill` | `"x"` | Frame `fill` passed to `pack` |
|
|
457
458
|
| `expand` | `False` | Frame `expand` passed to `pack` |
|
|
458
459
|
| `sync_yscroll` | `False` | Wire the two widgets' y-scroll commands together |
|
|
460
|
+
| `line_numbers` | `False` | Add read-only line-number gutters to both sides |
|
|
459
461
|
| `side`, `padx`, `pady`, `anchor` | — | Standard `Layout` block placement options |
|
|
460
462
|
|
|
461
463
|
When `sync_yscroll=True` and both widgets are `@app.text` widgets, scrolling
|
|
@@ -464,14 +466,21 @@ achieved per-widget with `@app.text(..., sync_yscroll_with="other")`; paired
|
|
|
464
466
|
layout provides a layout-level switch that works without editing widget
|
|
465
467
|
declarations.
|
|
466
468
|
|
|
467
|
-
|
|
469
|
+
With `line_numbers=True` each side gains a read-only line-number gutter. Both
|
|
470
|
+
panes and both gutters share a **single vertical scrollbar**, so the gutters
|
|
471
|
+
stay in lock-step with the content (no drift). Logical line numbers
|
|
472
|
+
(`1..n`) follow the pane content; the right pane's numbers update as you edit.
|
|
468
473
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
`
|
|
474
|
+
|
|
475
|
+
### Wrap layout
|
|
476
|
+
|
|
477
|
+
`Layout.wrap()` is a wrapping flow (Flutter `Wrap` analog): widgets are not
|
|
478
|
+
stretched to fill a column; each keeps the width implied by its content or
|
|
479
|
+
`width=` and they lay out left to right, wrapping to the next row whenever the
|
|
480
|
+
next widget no longer fits in the remaining frame width. Widgets in the same
|
|
481
|
+
row are vertically centered on their midline, so a tall `entry` aligns cleanly
|
|
482
|
+
with a shorter `button` or `checkbutton`. The gap inherits the layout spacing
|
|
483
|
+
by default (`gapx`/`gapy` override it independently).
|
|
475
484
|
|
|
476
485
|
```python
|
|
477
486
|
TAGS = ["python", "tkinter", "async", "uv", "type hints"]
|
|
@@ -480,13 +489,76 @@ for tag in TAGS:
|
|
|
480
489
|
def on_tag(values, tag=tag):
|
|
481
490
|
return {"msg": f"Selected: {tag}"}
|
|
482
491
|
|
|
483
|
-
app.run(layout=Layout(spacing=2).status("msg").
|
|
492
|
+
app.run(layout=Layout(spacing=2).status("msg").wrap(*TAGS))
|
|
484
493
|
```
|
|
485
494
|
|
|
486
|
-
|
|
487
|
-
rows automatically when the window is resized. Pass `
|
|
488
|
-
spacing default, and `side`/`fill`/`expand` to control how the
|
|
489
|
-
packed.
|
|
495
|
+
Wrap is ideal for tag clouds, toolbars, and filter UIs. The layout recomputes
|
|
496
|
+
rows automatically when the window is resized. Pass `gapx`/`gapy` to override
|
|
497
|
+
the spacing default, and `side`/`fill`/`expand` to control how the wrap frame
|
|
498
|
+
is packed.
|
|
499
|
+
|
|
500
|
+
Any wrap child wrapped in `Flex(name, flex=...)` absorbs leftover horizontal
|
|
501
|
+
space in its row (Flutter `Expanded` analog):
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
from nextpytk import Flex
|
|
505
|
+
|
|
506
|
+
app.run(layout=Layout().wrap("filter", Flex("search", flex=2), "ok", gapx=2))
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
For custom positioning, `Layout.flow()` takes a `FlowDelegate` (Flutter `Flow`
|
|
510
|
+
analog) that computes each child's `(x, y, width, height)` from the available
|
|
511
|
+
`Constraints`. See `examples/wrap_demo.py` for both `Flex` and `Flow` usage.
|
|
512
|
+
|
|
513
|
+
**How it works.** `wrap` and `flow` position children with `place` (absolute
|
|
514
|
+
x/y) because `pack` cannot reflow onto new rows — with `pack -side left` an
|
|
515
|
+
overflowing child simply falls off the edge. `place` therefore implies a few
|
|
516
|
+
constraints:
|
|
517
|
+
|
|
518
|
+
- All children share one parent frame and are `place`-managed; they cannot
|
|
519
|
+
also be `pack`/`grid`-managed on the same master (mixing geometry managers
|
|
520
|
+
raises `TclError: conflicting geometry managers`).
|
|
521
|
+
- Widths come from `winfo_reqwidth()`; the frame uses `pack_propagate(False)`
|
|
522
|
+
and its height is explicit.
|
|
523
|
+
- On resize the flow is recomputed so rows re-wrap to the new width.
|
|
524
|
+
|
|
525
|
+
Because `place` bypasses Tk's native Tab traversal (which follows pack/grid
|
|
526
|
+
insertion order), `wrap` intercepts `<Tab>`/`<Shift-Tab>` and moves focus in
|
|
527
|
+
the visual row-major order instead. `Entry`/`Text` children are skipped (Tab
|
|
528
|
+
stays for text input); focus wraps between the first and last wrap item.
|
|
529
|
+
|
|
530
|
+
> **Note:** `Layout.cluster()` was renamed to `Layout.wrap()` in 0.5.0.
|
|
531
|
+
> `cluster()` remains as a deprecated alias and will be removed in 0.5.0.
|
|
532
|
+
|
|
533
|
+
### Dynamic region switching (swap targets)
|
|
534
|
+
|
|
535
|
+
`Layout.target()` reserves a region whose contents change at runtime, mirroring
|
|
536
|
+
HTMX `hx-target` / `hx-swap`. Surrounding sections (toolbar, status) stay fixed
|
|
537
|
+
while only the target region swaps:
|
|
538
|
+
|
|
539
|
+
```python
|
|
540
|
+
layout = Layout().wrap("go_dir", "go_file", "info").target("main_area")
|
|
541
|
+
|
|
542
|
+
@app.swap(
|
|
543
|
+
"main_area",
|
|
544
|
+
variants={
|
|
545
|
+
"dir": [Layout().section("dir_tree")],
|
|
546
|
+
"file": [Layout().paired("left_text", "right_text",
|
|
547
|
+
fill=Fill.BOTH, expand=True)],
|
|
548
|
+
},
|
|
549
|
+
default="dir",
|
|
550
|
+
)
|
|
551
|
+
def main_area():
|
|
552
|
+
pass
|
|
553
|
+
|
|
554
|
+
# Switch imperatively at runtime:
|
|
555
|
+
app.swap_view("main_area", "file")
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
`@app.swap` variants are mounted up-front and shown/hidden via pack, so widget
|
|
559
|
+
state (e.g. a treeview selection or scroll position) survives switching. Each
|
|
560
|
+
variant is a `Layout` or a list of widget names. See
|
|
561
|
+
`examples/swap_demo.py`.
|
|
490
562
|
|
|
491
563
|
### Fluent DSL
|
|
492
564
|
|
|
@@ -576,7 +648,7 @@ app.run(layout=b.build())
|
|
|
576
648
|
| `@app.combobox(name, values=..., values_key=..., readonly=..., font=...)` | ttk.Combobox | selected value `str` | `dict` |
|
|
577
649
|
| `@app.menubar(name)` | tk.Menu (window menubar) | — | menu item list |
|
|
578
650
|
| `@app.filepicker(name, mode=..., label=..., title=..., filetypes=..., ...)` | ttk.Button → tkinter.filedialog | selected path(s) `str`, `list[str]`, or `None` | `dict` |
|
|
579
|
-
| `@app.text(name, width=..., height=..., font=...)` | tk.Text | full content `str` | `dict` |
|
|
651
|
+
| `@app.text(name, width=..., height=..., font=..., wrap=..., h_scroll=...)` | tk.Text | full content `str` | `dict` |
|
|
580
652
|
| `@app.scale(name, from_=..., to=..., orient=...)` | ttk.Scale | value `str` | `dict` |
|
|
581
653
|
| `@app.spinbox(name, from_=..., to=..., values=..., font=...)` | ttk.Spinbox | value `str` | `dict` |
|
|
582
654
|
| `@app.listbox(name, items=..., items_key=..., selectmode=..., font=..., events=...)` | tk.Listbox | selected index `int` (`-1` if none) | `dict` |
|
|
@@ -627,6 +699,32 @@ Entry options: `placeholder`, `show`, `font`, `padding`, `width`, `state`.
|
|
|
627
699
|
Button, checkbutton, radiobutton, spinbox, combobox, listbox, text options:
|
|
628
700
|
- `font` is accepted uniformly. Under the hood, ttk widgets use a derived style so the rest of the theme (colors, maps, layout) is inherited.
|
|
629
701
|
|
|
702
|
+
Text options (continued):
|
|
703
|
+
- `wrap`: `Wrap.WORD` (default) / `Wrap.NONE` / `Wrap.CHAR`. `Wrap.NONE` keeps each logical line on a single row.
|
|
704
|
+
- `h_scroll`: when `True`, a horizontal scrollbar is added (and wired to `xscrollcommand`) so long lines are reachable, typically used with `wrap=Wrap.NONE`.
|
|
705
|
+
|
|
706
|
+
Every widget decorator also accepts `widget_kwargs: dict` for per-widget
|
|
707
|
+
design-token overrides applied after construction. Keys are widget-native
|
|
708
|
+
tk/ttk options (`padx`, `pady`, `bg`, `fg`, `font`, …). An invalid/unknown
|
|
709
|
+
key is ignored rather than aborting the build:
|
|
710
|
+
|
|
711
|
+
```python
|
|
712
|
+
@app.text("log", wrap="none", h_scroll=True,
|
|
713
|
+
widget_kwargs={"bg": "#1e1e1e", "fg": "#dcdcdc"})
|
|
714
|
+
def log(value): return {}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
Runtime access: `app.text_widget(name)` returns the real `tk.Text`;
|
|
718
|
+
`app.layout_frame(name)` returns the layout section frame that owns a
|
|
719
|
+
widget (for manual grid/pack placement or re-parenting).
|
|
720
|
+
`app.on_text_set(name, hook)` registers a callback to run after a text
|
|
721
|
+
widget's content is replaced (used by paired line-number gutters).
|
|
722
|
+
|
|
723
|
+
Enum-like options (`wrap`, `state`, `orient`, `selectmode`, `mode`) are
|
|
724
|
+
validated at registration time: an invalid value raises a clear `ValueError`
|
|
725
|
+
naming the option and the allowed values, instead of failing later with a
|
|
726
|
+
`TclError` when the widget is built.
|
|
727
|
+
|
|
630
728
|
`@app.message` creates an auto-wrapping label. `width` sets initial pixel width; `auto_width=True` (default) tracks parent container resize.
|
|
631
729
|
|
|
632
730
|
---
|
|
@@ -686,6 +784,18 @@ print(app.debug_layout())
|
|
|
686
784
|
# → {"title": "...", "sections": [{"widgets": [{"name": "msg", "geometry": ..., ...}, ...]}]}
|
|
687
785
|
```
|
|
688
786
|
|
|
787
|
+
**Detecting geometry-manager conflicts.** Mixing `pack`/`grid`/`place` on one
|
|
788
|
+
master (e.g. manually `pack`/`grid`-ing into a `place`-managed `wrap`/`flow`
|
|
789
|
+
frame) cannot be fully prevented by the DSL, so `app.check_layout_conflicts()`
|
|
790
|
+
inspects the widget tree and reports any such mix without waiting for a
|
|
791
|
+
`TclError: conflicting geometry managers`:
|
|
792
|
+
|
|
793
|
+
```python
|
|
794
|
+
print(app.check_layout_conflicts())
|
|
795
|
+
# → [{"master_class": "Frame", "managers": ["pack", "place"], ...}] on conflict
|
|
796
|
+
# (a warning is also emitted for each conflict)
|
|
797
|
+
```
|
|
798
|
+
|
|
689
799
|
---
|
|
690
800
|
|
|
691
801
|
## Async-Native (asyncio + Tkinter)
|
|
@@ -717,6 +827,7 @@ uv run python examples/menubar_demo.py # menubar
|
|
|
717
827
|
uv run python examples/filepicker_demo.py # file picker
|
|
718
828
|
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
719
829
|
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
830
|
+
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
720
831
|
```
|
|
721
832
|
|
|
722
833
|
---
|
|
@@ -141,7 +141,119 @@ PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
|
141
141
|
|
|
142
142
|
---
|
|
143
143
|
|
|
144
|
-
##
|
|
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
|
+
## Near term (post-0.4.5)
|
|
145
257
|
|
|
146
258
|
### A11y
|
|
147
259
|
|
|
@@ -158,9 +270,11 @@ PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
|
158
270
|
### Widget expansion
|
|
159
271
|
|
|
160
272
|
- [x] Per-widget `bind` for entry and listbox (`events=` on `@app.entry` and `@app.listbox`)
|
|
273
|
+
- [x] Per-widget `bind` for other widgets via `widget_kwargs` / widget-level options
|
|
161
274
|
- [ ] Per-widget `bind` for other widgets (`text`, `combobox`, `treeview`, `button`, etc.)
|
|
162
275
|
- [x] listbox callback value design: keep selection index (unify with `treeview`)
|
|
163
276
|
- [x] `@app.filepicker` — `filedialog` wrapper (schema/tool naming still open)
|
|
277
|
+
- [x] Text `wrap=` / `h_scroll=` options (v0.4.4)
|
|
164
278
|
|
|
165
279
|
### Layout DSL
|
|
166
280
|
|
|
@@ -169,6 +283,11 @@ PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
|
169
283
|
- [x] Nested frames (`Layout` inside `Layout`) via `Layout.frame(...)`
|
|
170
284
|
- [x] Global default for `padx`/`pady` via `Layout(spacing=...)`
|
|
171
285
|
- [x] `Layout.cluster(...)` wrapping flow; `Layout.paired(..., sync_yscroll=)`
|
|
286
|
+
- [x] `Layout.cluster(...)` → `Layout.wrap(...)` rename with deprecated alias (v0.4.5)
|
|
287
|
+
- [x] `Flex` (Flutter `Expanded` analog) for `wrap` (v0.4.5)
|
|
288
|
+
- [x] `FlowDelegate` / `Layout.flow` custom flow (Flutter `Flow` analog) (v0.4.5)
|
|
289
|
+
- [x] `Layout.paired(..., line_numbers=True)` read-only line-number gutters (v0.4.4)
|
|
290
|
+
- [x] `Layout.target(...)` + `@app.swap(...)` dynamic region switching (v0.4.4)
|
|
172
291
|
|
|
173
292
|
### Agent / LLM integration
|
|
174
293
|
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
import importlib.metadata
|
|
4
4
|
|
|
5
5
|
from nextpytk.app import TkApp
|
|
6
|
-
from nextpytk.layout import
|
|
6
|
+
from nextpytk.layout import (
|
|
7
|
+
Constraints,
|
|
8
|
+
FlowDelegate,
|
|
9
|
+
Layout,
|
|
10
|
+
LayoutBuilder,
|
|
11
|
+
)
|
|
12
|
+
from nextpytk.types import Flex
|
|
7
13
|
from nextpytk import types
|
|
8
14
|
from nextpytk.widgets import WidgetSpec
|
|
9
15
|
from nextpytk.theme import (
|
|
@@ -31,6 +37,9 @@ __all__ = [
|
|
|
31
37
|
"TkApp",
|
|
32
38
|
"Layout",
|
|
33
39
|
"LayoutBuilder",
|
|
40
|
+
"Constraints",
|
|
41
|
+
"FlowDelegate",
|
|
42
|
+
"Flex",
|
|
34
43
|
"WidgetSpec",
|
|
35
44
|
"types",
|
|
36
45
|
"apply_theme",
|