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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextpytk
3
- Version: 0.4.1
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 same structure for agents and tools.
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, Layout
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(values):
54
+ def on_greet():
49
55
  return {"msg": "Button clicked!"}
50
56
 
51
- app.run(layout=Layout().section("msg").section("greet"))
57
+ app.run(layout=["msg", "greet"])
52
58
  ```
53
59
 
54
- **Three layout styles — pick the one that fits:**
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
- # 2) Fluent DSL
61
- app.run(layout=Layout().section("msg").section("greet"))
86
+ When you press the button, the callback returns this `dict`:
62
87
 
63
- # 3) with-block (context manager)
64
- with app.layout() as b:
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. This is explicit and less error-prone than index-position bulk lists.
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 item `str` | `dict` |
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 (Agent/LLM)
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(): return "25°C"
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; also handy to hand to an agent).
563
+ and layout regressions).
348
564
 
349
565
  ```python
350
566
  app.run(layout=["msg", "go"]) # or build via tests / custom runner