nextpytk 0.4.1__tar.gz → 0.4.2__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.2}/PKG-INFO +240 -24
- {nextpytk-0.4.1 → nextpytk-0.4.2}/README.md +239 -23
- {nextpytk-0.4.1 → nextpytk-0.4.2}/ROADMAP.md +54 -2
- {nextpytk-0.4.1 → nextpytk-0.4.2}/pyproject.toml +1 -1
- {nextpytk-0.4.1 → nextpytk-0.4.2}/src/nextpytk/app.py +337 -41
- {nextpytk-0.4.1 → nextpytk-0.4.2}/src/nextpytk/types.py +244 -15
- {nextpytk-0.4.1 → nextpytk-0.4.2}/.gitignore +0 -0
- {nextpytk-0.4.1 → nextpytk-0.4.2}/LICENSE +0 -0
- {nextpytk-0.4.1 → nextpytk-0.4.2}/src/nextpytk/__init__.py +0 -0
- {nextpytk-0.4.1 → nextpytk-0.4.2}/src/nextpytk/layout.py +0 -0
- {nextpytk-0.4.1 → nextpytk-0.4.2}/src/nextpytk/theme.py +0 -0
- {nextpytk-0.4.1 → nextpytk-0.4.2}/src/nextpytk/tokens.py +0 -0
- {nextpytk-0.4.1 → nextpytk-0.4.2}/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.2
|
|
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"])
|
|
52
58
|
```
|
|
53
59
|
|
|
54
|
-
|
|
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")
|
|
70
|
+
```
|
|
71
|
+
|
|
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
|
|
@@ -229,7 +440,7 @@ Grid builder methods:
|
|
|
229
440
|
| `row_minsize(row, px)` | Row minimum height |
|
|
230
441
|
| `end_grid()` | Return to Layout chain |
|
|
231
442
|
|
|
232
|
-
Use `col_weight(0, 0).col_weight(1, 1)` to set column 0 → weight 0 and column 1 → weight 1.
|
|
443
|
+
Use `col_weight(0, 0).col_weight(1, 1)` to set column 0 → weight 0 and column 1 → weight 1.
|
|
233
444
|
|
|
234
445
|
### With-block (context manager)
|
|
235
446
|
|
|
@@ -271,15 +482,15 @@ app.run(layout=b.build())
|
|
|
271
482
|
| `@app.message(name, width=..., auto_width=...)` | tk.Label (auto-wrap) | — | `str` or `dict` |
|
|
272
483
|
| `@app.button(name, label=..., font=..., enabled_if=...)` | ttk.Button | entry values `dict` | `dict` |
|
|
273
484
|
| `@app.job(name)` | async callable | entry values `dict` | `dict` |
|
|
274
|
-
| `@app.entry(name, placeholder=..., show=..., font=..., padding=..., width=...)` | ttk.Entry | `str` | `dict` |
|
|
485
|
+
| `@app.entry(name, placeholder=..., show=..., font=..., padding=..., width=..., events=...)` | ttk.Entry | `str` | `dict` |
|
|
275
486
|
| `@app.checkbutton(name, text=..., font=...)` | ttk.Checkbutton | `bool` | `dict` |
|
|
276
487
|
| `@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` |
|
|
488
|
+
| `@app.combobox(name, values=..., values_key=..., readonly=..., font=...)` | ttk.Combobox | selected value `str` | `dict` |
|
|
278
489
|
| `@app.menubar(name)` | tk.Menu (window menubar) | — | menu item list |
|
|
279
490
|
| `@app.text(name, width=..., height=..., font=...)` | tk.Text | full content `str` | `dict` |
|
|
280
491
|
| `@app.scale(name, from_=..., to=..., orient=...)` | ttk.Scale | value `str` | `dict` |
|
|
281
492
|
| `@app.spinbox(name, from_=..., to=..., values=..., font=...)` | ttk.Spinbox | value `str` | `dict` |
|
|
282
|
-
| `@app.listbox(name, items=..., selectmode=..., font=...)` | tk.Listbox | selected
|
|
493
|
+
| `@app.listbox(name, items=..., items_key=..., selectmode=..., font=..., events=...)` | tk.Listbox | selected index `int` (`-1` if none) | `dict` |
|
|
283
494
|
| `@app.canvas(name, width=..., height=...)` | tk.Canvas | — | — |
|
|
284
495
|
|
|
285
496
|
`@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.
|
|
@@ -323,28 +534,33 @@ accept raw strings too.
|
|
|
323
534
|
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/GROOVE/RIDGE/SOLID` | border style |
|
|
324
535
|
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
325
536
|
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
537
|
+
| `EventSeq` | `EventSeq.RETURN/ESCAPE/BACKSPACE/DELETE/TAB/...` | event sequences for bindings |
|
|
538
|
+
| `EventSeq` (mouse) | `EventSeq.BUTTON_1/2/3`, `DOUBLE_BUTTON_1/2/3`, `PRIMARY_DOUBLE_CLICK` | mouse event sequences |
|
|
539
|
+
| `EventSeq` (virtual) | `EventSeq.LISTBOX_SELECT/COMBOBOX_SELECTED/NOTEBOOK_TAB_CHANGED/...` | virtual events |
|
|
326
540
|
|
|
327
541
|
---
|
|
328
542
|
|
|
329
|
-
## Schema Export
|
|
543
|
+
## Schema Export
|
|
544
|
+
|
|
545
|
+
`app.schema()` returns a JSON-compatible snapshot of registered widgets
|
|
546
|
+
(`name`, `kind`, `label`, `role`, `description`, plus kind-specific fields).
|
|
330
547
|
|
|
331
548
|
```python
|
|
332
|
-
@label("temperature")
|
|
333
|
-
def t():
|
|
549
|
+
@app.label("temperature")
|
|
550
|
+
def t():
|
|
551
|
+
return "25°C"
|
|
334
552
|
|
|
335
553
|
app.schema()
|
|
336
554
|
# → {"title": "...", "widgets": [{"name": "temperature", "kind": "label", ...}]}
|
|
337
555
|
```
|
|
338
556
|
|
|
339
|
-
Output is JSON-compatible and can be used as a foundation for agent tools and LLM function definitions.
|
|
340
|
-
|
|
341
557
|
---
|
|
342
558
|
|
|
343
559
|
## Layout debug
|
|
344
560
|
|
|
345
561
|
After widgets are built, `app.debug_layout()` returns JSON-compatible geometry
|
|
346
562
|
and pack/grid info for every registered widget (useful for clipping, minsize,
|
|
347
|
-
and layout regressions
|
|
563
|
+
and layout regressions).
|
|
348
564
|
|
|
349
565
|
```python
|
|
350
566
|
app.run(layout=["msg", "go"]) # or build via tests / custom runner
|