nextpytk 0.4.1__tar.gz → 0.4.3__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.1 → nextpytk-0.4.3}/.gitignore +2 -1
- {nextpytk-0.4.1 → nextpytk-0.4.3}/PKG-INFO +384 -24
- {nextpytk-0.4.1 → nextpytk-0.4.3}/README.md +383 -23
- {nextpytk-0.4.1 → nextpytk-0.4.3}/ROADMAP.md +70 -5
- {nextpytk-0.4.1 → nextpytk-0.4.3}/pyproject.toml +1 -1
- {nextpytk-0.4.1 → nextpytk-0.4.3}/src/nextpytk/app.py +480 -47
- {nextpytk-0.4.1 → nextpytk-0.4.3}/src/nextpytk/layout.py +603 -35
- {nextpytk-0.4.1 → nextpytk-0.4.3}/src/nextpytk/types.py +265 -15
- {nextpytk-0.4.1 → nextpytk-0.4.3}/src/nextpytk/widgets.py +1 -0
- {nextpytk-0.4.1 → nextpytk-0.4.3}/LICENSE +0 -0
- {nextpytk-0.4.1 → nextpytk-0.4.3}/src/nextpytk/__init__.py +0 -0
- {nextpytk-0.4.1 → nextpytk-0.4.3}/src/nextpytk/theme.py +0 -0
- {nextpytk-0.4.1 → nextpytk-0.4.3}/src/nextpytk/tokens.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.3
|
|
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
|
|
@@ -28,15 +28,21 @@ Accessible, declarative Tkinter applications from ordinary Python functions.
|
|
|
28
28
|
|
|
29
29
|
Register widgets as plain Python functions, declare layout separately, and
|
|
30
30
|
keep roles/descriptions in one place. The decorator style is Flask-inspired;
|
|
31
|
-
`schema()` exports the
|
|
31
|
+
`schema()` exports the registered widget structure as JSON-compatible data.
|
|
32
32
|
Uses ttk widgets where available.
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
36
|
## Quick Start
|
|
37
37
|
|
|
38
|
+
Start with an app where a button updates a message.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install nextpytk
|
|
42
|
+
```
|
|
43
|
+
|
|
38
44
|
```python
|
|
39
|
-
from nextpytk import TkApp
|
|
45
|
+
from nextpytk import TkApp
|
|
40
46
|
|
|
41
47
|
app = TkApp(title="Hello")
|
|
42
48
|
|
|
@@ -45,28 +51,144 @@ def msg():
|
|
|
45
51
|
return "Hello, world!"
|
|
46
52
|
|
|
47
53
|
@app.button("greet", label="Greet")
|
|
48
|
-
def on_greet(
|
|
54
|
+
def on_greet():
|
|
49
55
|
return {"msg": "Button clicked!"}
|
|
50
56
|
|
|
51
|
-
app.run(layout=
|
|
57
|
+
app.run(layout=["msg", "greet"])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## How it works
|
|
63
|
+
|
|
64
|
+
In nextpytk you register widgets by name and declare layout separately.
|
|
65
|
+
|
|
66
|
+
This example registers two widgets: `msg` and `greet`.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
@app.status("msg")
|
|
52
70
|
```
|
|
53
71
|
|
|
54
|
-
|
|
72
|
+
`msg` is a status area that shows a message.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
@app.button("greet", label="Greet")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`greet` is a button labeled "Greet".
|
|
79
|
+
|
|
80
|
+
Finally, name the display order:
|
|
55
81
|
|
|
56
82
|
```python
|
|
57
|
-
# 1) Simple list (easiest)
|
|
58
83
|
app.run(layout=["msg", "greet"])
|
|
84
|
+
```
|
|
59
85
|
|
|
60
|
-
|
|
61
|
-
app.run(layout=Layout().section("msg").section("greet"))
|
|
86
|
+
When you press the button, the callback returns this `dict`:
|
|
62
87
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
b.section("msg")
|
|
66
|
-
b.section("greet")
|
|
67
|
-
app.run(layout=b.build())
|
|
88
|
+
```python
|
|
89
|
+
{"msg": "Button clicked!"}
|
|
68
90
|
```
|
|
69
91
|
|
|
92
|
+
nextpytk merges that into the app `state`.
|
|
93
|
+
|
|
94
|
+
Because `msg` is the name of the registered status area, its text becomes
|
|
95
|
+
"Button clicked!".
|
|
96
|
+
|
|
97
|
+
So the basic flow is:
|
|
98
|
+
|
|
99
|
+
* Register widgets by name
|
|
100
|
+
* List those names in the layout
|
|
101
|
+
* Return a `dict` from a callback
|
|
102
|
+
* `state` updates and matching widgets refresh
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Using input values
|
|
107
|
+
|
|
108
|
+
Next, change the app so you type a name and press Greet.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from nextpytk import TkApp
|
|
112
|
+
|
|
113
|
+
app = TkApp(title="Hello")
|
|
114
|
+
|
|
115
|
+
@app.entry("name", placeholder="Name")
|
|
116
|
+
def on_name():
|
|
117
|
+
return {}
|
|
118
|
+
|
|
119
|
+
@app.status("msg")
|
|
120
|
+
def msg():
|
|
121
|
+
return "Enter your name"
|
|
122
|
+
|
|
123
|
+
@app.button("greet", label="Greet")
|
|
124
|
+
def on_greet(values):
|
|
125
|
+
name = values["name"]
|
|
126
|
+
return {"msg": f"Hello, {name}!"}
|
|
127
|
+
|
|
128
|
+
app.run(layout=["name", "greet", "msg"])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`entry` registers a text field.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
@app.entry("name", placeholder="Name")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Again, `"name"` is the widget name. An on-change callback is required; if you
|
|
138
|
+
only read the field from a button, a no-arg callback that returns `{}` is enough.
|
|
139
|
+
|
|
140
|
+
In the button callback, read the current field value from `values`:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
def on_greet(values):
|
|
144
|
+
name = values["name"]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`values["name"]` is the current value of the `entry` registered as `name`.
|
|
148
|
+
|
|
149
|
+
For example, if you type
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
Taro
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
and press the button, the callback returns:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
{"msg": "Hello, Taro!"}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
That dict merges into `state` and updates the status area named `msg`.
|
|
162
|
+
|
|
163
|
+
## `values` and `state`
|
|
164
|
+
|
|
165
|
+
Two dictionaries appear here:
|
|
166
|
+
|
|
167
|
+
* `values`
|
|
168
|
+
Input values at the moment the callback runs
|
|
169
|
+
|
|
170
|
+
* `state`
|
|
171
|
+
Shared current state for the whole app
|
|
172
|
+
|
|
173
|
+
A button callback reads `values`, then returns a `dict` of state changes:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
def on_greet(values):
|
|
177
|
+
name = values["name"]
|
|
178
|
+
return {"msg": f"Hello, {name}!"}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The flow is:
|
|
182
|
+
|
|
183
|
+
* Type into an `entry`
|
|
184
|
+
* Press the button
|
|
185
|
+
* Read input from `values`
|
|
186
|
+
* Return updates as a `dict`
|
|
187
|
+
* Those updates merge into `state`
|
|
188
|
+
* Matching widgets refresh from `state`
|
|
189
|
+
|
|
190
|
+
That is nextpytk's basic state-update model.
|
|
191
|
+
|
|
70
192
|
---
|
|
71
193
|
|
|
72
194
|
## Design System
|
|
@@ -91,9 +213,80 @@ Layout().section("msg", side=Side.LEFT, fill=Fill.X)
|
|
|
91
213
|
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/...` | border style |
|
|
92
214
|
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
93
215
|
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
216
|
+
| `EventSeq` | `EventSeq.RETURN/ESCAPE/BACKSPACE/DELETE/TAB/...` | event sequences for bindings |
|
|
217
|
+
| `EventSeq` (mouse) | `EventSeq.BUTTON_1/2/3`, `DOUBLE_BUTTON_1/2/3`, `PRIMARY_DOUBLE_CLICK` | mouse event sequences |
|
|
218
|
+
| `EventSeq` (virtual) | `EventSeq.LISTBOX_SELECT/COMBOBOX_SELECTED/NOTEBOOK_TAB_CHANGED/...` | virtual events |
|
|
94
219
|
|
|
95
220
|
Each type has a matching `*Like` literal alias (e.g. `FillLike`), so raw strings still work when needed.
|
|
96
221
|
|
|
222
|
+
### Event sequences
|
|
223
|
+
|
|
224
|
+
Use `EventSeq` constants for widget-level event bindings (`events=` on
|
|
225
|
+
`@app.listbox` and `@app.entry`). These handlers are **separate from** the
|
|
226
|
+
widget's select/change callback:
|
|
227
|
+
|
|
228
|
+
| API | Handler argument | Typical use |
|
|
229
|
+
|-----|------------------|-------------|
|
|
230
|
+
| `@app.listbox` select callback | selected index `int` (`-1` if none) | react to selection |
|
|
231
|
+
| `@app.listbox(..., events=...)` | current **state** dict | Return / double-click / Backspace |
|
|
232
|
+
| `@app.entry` change callback | value `str` | react to typing |
|
|
233
|
+
| `@app.entry(..., events=...)` | **entry values** dict (all entries) | Return to submit, like a button |
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from nextpytk.types import EventSeq, SelectMode
|
|
237
|
+
|
|
238
|
+
@app.listbox(
|
|
239
|
+
"results",
|
|
240
|
+
items_key="results_items",
|
|
241
|
+
selectmode=SelectMode.BROWSE,
|
|
242
|
+
events={
|
|
243
|
+
EventSeq.RETURN: lambda state: open_child(),
|
|
244
|
+
EventSeq.PRIMARY_DOUBLE_CLICK: lambda state: open_child(),
|
|
245
|
+
EventSeq.BACKSPACE: lambda state: go_parent(),
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
def on_results_select(idx: int) -> dict:
|
|
249
|
+
return {"status": f"selected:{idx}"}
|
|
250
|
+
|
|
251
|
+
@app.entry(
|
|
252
|
+
"query",
|
|
253
|
+
events={
|
|
254
|
+
EventSeq.RETURN: lambda values: {
|
|
255
|
+
"status": f"search:{(values.get('query') or '').strip()}"
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
def on_query(value: str) -> dict:
|
|
260
|
+
return {}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`EventSeq.PRIMARY_CLICK`, `EventSeq.PRIMARY_DOUBLE_CLICK`, and
|
|
264
|
+
`EventSeq.PRIMARY_BUTTON_RELEASE` are a11y-aware lazy descriptors that
|
|
265
|
+
return the event sequence for the OS-configured primary mouse button
|
|
266
|
+
(accounting for left/right button swap on Windows, macOS, and Linux/GNOME).
|
|
267
|
+
|
|
268
|
+
### Dynamic choices
|
|
269
|
+
|
|
270
|
+
State-driven choices mirror treeview's `rows_key`: keep the selection in
|
|
271
|
+
`state[name]` / `state[key]`, and refresh the options via a separate key.
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
@app.listbox("results", items_key="results_items")
|
|
275
|
+
def on_results_select(idx: int) -> dict:
|
|
276
|
+
return {}
|
|
277
|
+
|
|
278
|
+
@app.combobox("folder", values_key="folder_values")
|
|
279
|
+
def on_folder(value: str) -> dict:
|
|
280
|
+
return {}
|
|
281
|
+
|
|
282
|
+
# Later (button, job, or initial_state):
|
|
283
|
+
app.apply_state({
|
|
284
|
+
"results_items": ["a", "b", "c"],
|
|
285
|
+
"folder_values": ["INBOX", "Sent"],
|
|
286
|
+
})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Omit `items_key` / `values_key` to keep a static `items=` / `values=` list.
|
|
97
290
|
### Spacing tokens
|
|
98
291
|
|
|
99
292
|
```python
|
|
@@ -177,6 +370,24 @@ view_layouts = {
|
|
|
177
370
|
|
|
178
371
|
## Layout DSL
|
|
179
372
|
|
|
373
|
+
Three styles — pick the one that fits:
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
from nextpytk import Layout
|
|
377
|
+
|
|
378
|
+
# 1) Simple list (easiest)
|
|
379
|
+
app.run(layout=["msg", "greet"])
|
|
380
|
+
|
|
381
|
+
# 2) Fluent DSL
|
|
382
|
+
app.run(layout=Layout().section("msg").section("greet"))
|
|
383
|
+
|
|
384
|
+
# 3) with-block (context manager)
|
|
385
|
+
with app.layout() as b:
|
|
386
|
+
b.section("msg")
|
|
387
|
+
b.section("greet")
|
|
388
|
+
app.run(layout=b.build())
|
|
389
|
+
```
|
|
390
|
+
|
|
180
391
|
### Simple list
|
|
181
392
|
|
|
182
393
|
```python
|
|
@@ -189,6 +400,118 @@ Each name gets its own pack-based section. Extra kwargs forwarded to `section()`
|
|
|
189
400
|
Layout.from_list(["a", "b"], fill="both", expand=True)
|
|
190
401
|
```
|
|
191
402
|
|
|
403
|
+
### Layout spacing
|
|
404
|
+
|
|
405
|
+
The default block padding comes from the design token `SPACE[1]` (4 px). Pass
|
|
406
|
+
`spacing=...` to a `Layout` constructor to change the default padding for every
|
|
407
|
+
block it creates:
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
from nextpytk import tokens
|
|
411
|
+
|
|
412
|
+
layout = Layout(spacing=2).section("a").section("b").grid().widget("c").end_grid()
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`spacing` accepts a key from `nextpytk.tokens.SPACE` (1, 2, 3, 4, 6, 8). It sets
|
|
416
|
+
the default `padx` and `pady` used by `section()`, `grid()`, `paned()`, grid
|
|
417
|
+
`widget()` placements, and nested frames. Explicit `padx`/`pady` arguments still
|
|
418
|
+
override the default.
|
|
419
|
+
|
|
420
|
+
For finer control, pass `padx` or `pady` directly to the constructor:
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
Layout(padx=tokens.SPACE[4], pady=tokens.SPACE[3]).section("a")
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Nested frames
|
|
427
|
+
|
|
428
|
+
A `Layout` can be placed inside a named frame to group widgets visually without
|
|
429
|
+
mixing pack and grid in the same parent. The inner layout owns its own spacing
|
|
430
|
+
and may use any layout style (pack sections, grid, paned, or further nested
|
|
431
|
+
frames).
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
inner = Layout().section("a", "b")
|
|
435
|
+
outer = Layout().section("title").frame("group", inner).section("ok")
|
|
436
|
+
app.run(layout=outer)
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
`frame(name, layout, side=..., fill=..., expand=..., padx=..., pady=...)` packs
|
|
440
|
+
the inner layout as a single block. The frame itself is registered under `name`,
|
|
441
|
+
so it can also be placed inside a grid cell:
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
outer = (
|
|
445
|
+
Layout()
|
|
446
|
+
.grid()
|
|
447
|
+
.widget("label")
|
|
448
|
+
.widget("group", sticky="nsew")
|
|
449
|
+
.end_grid()
|
|
450
|
+
.frame("group", Layout().section("a", "b"))
|
|
451
|
+
)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Paired layout
|
|
455
|
+
|
|
456
|
+
`Layout.paired(left, right, ...)` places two widgets side by side in a
|
|
457
|
+
single two-column frame. It is a lighter alternative to `app.paned` for
|
|
458
|
+
diff/compare views:
|
|
459
|
+
|
|
460
|
+
```python
|
|
461
|
+
layout = (
|
|
462
|
+
Layout()
|
|
463
|
+
.section("info")
|
|
464
|
+
.paired(
|
|
465
|
+
"left_text",
|
|
466
|
+
"right_text",
|
|
467
|
+
weight=(1, 1),
|
|
468
|
+
fill=Fill.BOTH,
|
|
469
|
+
expand=True,
|
|
470
|
+
sync_yscroll=True,
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Options:
|
|
476
|
+
|
|
477
|
+
| Argument | Default | Description |
|
|
478
|
+
|----------|---------|-------------|
|
|
479
|
+
| `weight` | `(1, 1)` | Two-column `(left, right)` grid weights |
|
|
480
|
+
| `fill` | `"x"` | Frame `fill` passed to `pack` |
|
|
481
|
+
| `expand` | `False` | Frame `expand` passed to `pack` |
|
|
482
|
+
| `sync_yscroll` | `False` | Wire the two widgets' y-scroll commands together |
|
|
483
|
+
| `side`, `padx`, `pady`, `anchor` | — | Standard `Layout` block placement options |
|
|
484
|
+
|
|
485
|
+
When `sync_yscroll=True` and both widgets are `@app.text` widgets, scrolling
|
|
486
|
+
either pane moves the other. For text widgets the same effect can also be
|
|
487
|
+
achieved per-widget with `@app.text(..., sync_yscroll_with="other")`; paired
|
|
488
|
+
layout provides a layout-level switch that works without editing widget
|
|
489
|
+
declarations.
|
|
490
|
+
|
|
491
|
+
### Cluster layout
|
|
492
|
+
|
|
493
|
+
`Layout.cluster()` is a wrapping flow: widgets are not stretched to fill a
|
|
494
|
+
column; each keeps the width implied by its content or `width=` and they lay
|
|
495
|
+
out left to right, wrapping to the next row whenever the next widget no longer
|
|
496
|
+
fits in the remaining frame width. Widgets in the same row are vertically
|
|
497
|
+
centered on their midline, so a tall `entry` aligns cleanly with a shorter
|
|
498
|
+
`button` or `checkbutton`. The gap inherits the layout spacing by default.
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
TAGS = ["python", "tkinter", "async", "uv", "type hints"]
|
|
502
|
+
for tag in TAGS:
|
|
503
|
+
@app.button(tag, label=f"#{tag}")
|
|
504
|
+
def on_tag(values, tag=tag):
|
|
505
|
+
return {"msg": f"Selected: {tag}"}
|
|
506
|
+
|
|
507
|
+
app.run(layout=Layout(spacing=2).status("msg").cluster(*TAGS))
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Cluster is ideal for tag clouds, toolbars, and filter UIs. The layout recomputes
|
|
511
|
+
rows automatically when the window is resized. Pass `gap=...` to override the
|
|
512
|
+
spacing default, and `side`/`fill`/`expand` to control how the cluster frame is
|
|
513
|
+
packed.
|
|
514
|
+
|
|
192
515
|
### Fluent DSL
|
|
193
516
|
|
|
194
517
|
**Pack sections:**
|
|
@@ -229,7 +552,7 @@ Grid builder methods:
|
|
|
229
552
|
| `row_minsize(row, px)` | Row minimum height |
|
|
230
553
|
| `end_grid()` | Return to Layout chain |
|
|
231
554
|
|
|
232
|
-
Use `col_weight(0, 0).col_weight(1, 1)` to set column 0 → weight 0 and column 1 → weight 1.
|
|
555
|
+
Use `col_weight(0, 0).col_weight(1, 1)` to set column 0 → weight 0 and column 1 → weight 1.
|
|
233
556
|
|
|
234
557
|
### With-block (context manager)
|
|
235
558
|
|
|
@@ -271,19 +594,49 @@ app.run(layout=b.build())
|
|
|
271
594
|
| `@app.message(name, width=..., auto_width=...)` | tk.Label (auto-wrap) | — | `str` or `dict` |
|
|
272
595
|
| `@app.button(name, label=..., font=..., enabled_if=...)` | ttk.Button | entry values `dict` | `dict` |
|
|
273
596
|
| `@app.job(name)` | async callable | entry values `dict` | `dict` |
|
|
274
|
-
| `@app.entry(name, placeholder=..., show=..., font=..., padding=..., width=...)` | ttk.Entry | `str` | `dict` |
|
|
597
|
+
| `@app.entry(name, placeholder=..., show=..., font=..., padding=..., width=..., events=...)` | ttk.Entry | `str` | `dict` |
|
|
275
598
|
| `@app.checkbutton(name, text=..., font=...)` | ttk.Checkbutton | `bool` | `dict` |
|
|
276
599
|
| `@app.radiobutton(name, text=..., value=..., group=..., font=...)` | ttk.Radiobutton | selected value `str` | `dict` |
|
|
277
|
-
| `@app.combobox(name, values=..., readonly=..., font=...)` | ttk.Combobox | selected value `str` | `dict` |
|
|
600
|
+
| `@app.combobox(name, values=..., values_key=..., readonly=..., font=...)` | ttk.Combobox | selected value `str` | `dict` |
|
|
278
601
|
| `@app.menubar(name)` | tk.Menu (window menubar) | — | menu item list |
|
|
602
|
+
| `@app.filepicker(name, mode=..., label=..., title=..., filetypes=..., ...)` | ttk.Button → tkinter.filedialog | selected path(s) `str`, `list[str]`, or `None` | `dict` |
|
|
279
603
|
| `@app.text(name, width=..., height=..., font=...)` | tk.Text | full content `str` | `dict` |
|
|
280
604
|
| `@app.scale(name, from_=..., to=..., orient=...)` | ttk.Scale | value `str` | `dict` |
|
|
281
605
|
| `@app.spinbox(name, from_=..., to=..., values=..., font=...)` | ttk.Spinbox | value `str` | `dict` |
|
|
282
|
-
| `@app.listbox(name, items=..., selectmode=..., font=...)` | tk.Listbox | selected
|
|
606
|
+
| `@app.listbox(name, items=..., items_key=..., selectmode=..., font=..., events=...)` | tk.Listbox | selected index `int` (`-1` if none) | `dict` |
|
|
283
607
|
| `@app.canvas(name, width=..., height=...)` | tk.Canvas | — | — |
|
|
284
608
|
|
|
285
609
|
`@app.status` sets schema / accessible `role="status"` metadata. It is **not** an ARIA live region yet (planned for a later release). Prefer it for operation feedback labels; use `@app.label` for static or high-frequency mirror text.
|
|
286
610
|
|
|
611
|
+
### File picker
|
|
612
|
+
|
|
613
|
+
`@app.filepicker` creates a button that opens a tkinter file dialog.
|
|
614
|
+
The selected path(s) are passed to the callback; `None` on cancel.
|
|
615
|
+
|
|
616
|
+
```python
|
|
617
|
+
@app.filepicker("open_file", mode="open", label="Open file",
|
|
618
|
+
title="Open file", filetypes=[("Text files", "*.txt")])
|
|
619
|
+
def pick_open_file(path: str | None) -> dict[str, Any]:
|
|
620
|
+
return {"open_path": path}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Modes: `"open"` (default), `"open_multiple"`, `"save"`, `"directory"`.
|
|
624
|
+
|
|
625
|
+
A filepicker can also be invoked from a menubar item by using its name
|
|
626
|
+
as the `command`:
|
|
627
|
+
|
|
628
|
+
```python
|
|
629
|
+
@app.filepicker("m_open", mode="open", title="Open file")
|
|
630
|
+
def m_open(path):
|
|
631
|
+
return {"open_path": path}
|
|
632
|
+
|
|
633
|
+
@app.menubar("menu")
|
|
634
|
+
def menu_bar():
|
|
635
|
+
return [{"label": "File", "items": [
|
|
636
|
+
{"label": "Open...", "command": "m_open"},
|
|
637
|
+
]}]
|
|
638
|
+
```
|
|
639
|
+
|
|
287
640
|
`app.run(stages=..., tabposition=...)` and `@app.stages` provide state-driven screen switching (one visible stage at a time). Theme helpers (`apply_theme`, `tokens`, layout chrome) ship in the package root — see `examples/header_demo.py`.
|
|
288
641
|
|
|
289
642
|
Common options:
|
|
@@ -323,28 +676,33 @@ accept raw strings too.
|
|
|
323
676
|
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/GROOVE/RIDGE/SOLID` | border style |
|
|
324
677
|
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
325
678
|
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
679
|
+
| `EventSeq` | `EventSeq.RETURN/ESCAPE/BACKSPACE/DELETE/TAB/...` | event sequences for bindings |
|
|
680
|
+
| `EventSeq` (mouse) | `EventSeq.BUTTON_1/2/3`, `DOUBLE_BUTTON_1/2/3`, `PRIMARY_DOUBLE_CLICK` | mouse event sequences |
|
|
681
|
+
| `EventSeq` (virtual) | `EventSeq.LISTBOX_SELECT/COMBOBOX_SELECTED/NOTEBOOK_TAB_CHANGED/...` | virtual events |
|
|
326
682
|
|
|
327
683
|
---
|
|
328
684
|
|
|
329
|
-
## Schema Export
|
|
685
|
+
## Schema Export
|
|
686
|
+
|
|
687
|
+
`app.schema()` returns a JSON-compatible snapshot of registered widgets
|
|
688
|
+
(`name`, `kind`, `label`, `role`, `description`, plus kind-specific fields).
|
|
330
689
|
|
|
331
690
|
```python
|
|
332
|
-
@label("temperature")
|
|
333
|
-
def t():
|
|
691
|
+
@app.label("temperature")
|
|
692
|
+
def t():
|
|
693
|
+
return "25°C"
|
|
334
694
|
|
|
335
695
|
app.schema()
|
|
336
696
|
# → {"title": "...", "widgets": [{"name": "temperature", "kind": "label", ...}]}
|
|
337
697
|
```
|
|
338
698
|
|
|
339
|
-
Output is JSON-compatible and can be used as a foundation for agent tools and LLM function definitions.
|
|
340
|
-
|
|
341
699
|
---
|
|
342
700
|
|
|
343
701
|
## Layout debug
|
|
344
702
|
|
|
345
703
|
After widgets are built, `app.debug_layout()` returns JSON-compatible geometry
|
|
346
704
|
and pack/grid info for every registered widget (useful for clipping, minsize,
|
|
347
|
-
and layout regressions
|
|
705
|
+
and layout regressions).
|
|
348
706
|
|
|
349
707
|
```python
|
|
350
708
|
app.run(layout=["msg", "go"]) # or build via tests / custom runner
|
|
@@ -380,7 +738,9 @@ uv run python examples/widget_gallery.py # all widget types
|
|
|
380
738
|
uv run python examples/header_demo.py # Layout.header / .status chrome
|
|
381
739
|
uv run python examples/combobox_demo.py # ttk.Combobox
|
|
382
740
|
uv run python examples/menubar_demo.py # menubar
|
|
741
|
+
uv run python examples/filepicker_demo.py # file picker
|
|
383
742
|
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
743
|
+
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
384
744
|
```
|
|
385
745
|
|
|
386
746
|
---
|