nextpytk 0.4.4__tar.gz → 0.4.6__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.4 → nextpytk-0.4.6}/PKG-INFO +61 -14
- {nextpytk-0.4.4 → nextpytk-0.4.6}/README.md +60 -13
- {nextpytk-0.4.4 → nextpytk-0.4.6}/ROADMAP.md +93 -1
- {nextpytk-0.4.4 → nextpytk-0.4.6}/pyproject.toml +1 -1
- {nextpytk-0.4.4 → nextpytk-0.4.6}/src/nextpytk/__init__.py +10 -1
- {nextpytk-0.4.4 → nextpytk-0.4.6}/src/nextpytk/app.py +69 -0
- {nextpytk-0.4.4 → nextpytk-0.4.6}/src/nextpytk/layout.py +576 -90
- {nextpytk-0.4.4 → nextpytk-0.4.6}/src/nextpytk/theme.py +24 -6
- {nextpytk-0.4.4 → nextpytk-0.4.6}/src/nextpytk/types.py +28 -0
- {nextpytk-0.4.4 → nextpytk-0.4.6}/.gitignore +0 -0
- {nextpytk-0.4.4 → nextpytk-0.4.6}/LICENSE +0 -0
- {nextpytk-0.4.4 → nextpytk-0.4.6}/src/nextpytk/tokens.py +0 -0
- {nextpytk-0.4.4 → nextpytk-0.4.6}/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.6
|
|
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
|
|
@@ -496,14 +496,15 @@ stay in lock-step with the content (no drift). Logical line numbers
|
|
|
496
496
|
(`1..n`) follow the pane content; the right pane's numbers update as you edit.
|
|
497
497
|
|
|
498
498
|
|
|
499
|
-
###
|
|
499
|
+
### Wrap layout
|
|
500
500
|
|
|
501
|
-
`Layout.
|
|
502
|
-
column; each keeps the width implied by its content or
|
|
503
|
-
out left to right, wrapping to the next row whenever the
|
|
504
|
-
fits in the remaining frame width. Widgets in the same
|
|
505
|
-
centered on their midline, so a tall `entry` aligns cleanly
|
|
506
|
-
`button` or `checkbutton`. The gap inherits the layout spacing
|
|
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).
|
|
507
508
|
|
|
508
509
|
```python
|
|
509
510
|
TAGS = ["python", "tkinter", "async", "uv", "type hints"]
|
|
@@ -512,13 +513,46 @@ for tag in TAGS:
|
|
|
512
513
|
def on_tag(values, tag=tag):
|
|
513
514
|
return {"msg": f"Selected: {tag}"}
|
|
514
515
|
|
|
515
|
-
app.run(layout=Layout(spacing=2).status("msg").
|
|
516
|
+
app.run(layout=Layout(spacing=2).status("msg").wrap(*TAGS))
|
|
516
517
|
```
|
|
517
518
|
|
|
518
|
-
|
|
519
|
-
rows automatically when the window is resized. Pass `
|
|
520
|
-
spacing default, and `side`/`fill`/`expand` to control how the
|
|
521
|
-
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.
|
|
522
556
|
|
|
523
557
|
### Dynamic region switching (swap targets)
|
|
524
558
|
|
|
@@ -527,7 +561,7 @@ HTMX `hx-target` / `hx-swap`. Surrounding sections (toolbar, status) stay fixed
|
|
|
527
561
|
while only the target region swaps:
|
|
528
562
|
|
|
529
563
|
```python
|
|
530
|
-
layout = Layout().
|
|
564
|
+
layout = Layout().wrap("go_dir", "go_file", "info").target("main_area")
|
|
531
565
|
|
|
532
566
|
@app.swap(
|
|
533
567
|
"main_area",
|
|
@@ -774,6 +808,18 @@ print(app.debug_layout())
|
|
|
774
808
|
# → {"title": "...", "sections": [{"widgets": [{"name": "msg", "geometry": ..., ...}, ...]}]}
|
|
775
809
|
```
|
|
776
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
|
+
|
|
777
823
|
---
|
|
778
824
|
|
|
779
825
|
## Async-Native (asyncio + Tkinter)
|
|
@@ -806,6 +852,7 @@ uv run python examples/filepicker_demo.py # file picker
|
|
|
806
852
|
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
807
853
|
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
808
854
|
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
855
|
+
uv run python examples/bottom_bar_demo.py # pinned bottom bar (section side="bottom")
|
|
809
856
|
```
|
|
810
857
|
|
|
811
858
|
---
|
|
@@ -472,14 +472,15 @@ stay in lock-step with the content (no drift). Logical line numbers
|
|
|
472
472
|
(`1..n`) follow the pane content; the right pane's numbers update as you edit.
|
|
473
473
|
|
|
474
474
|
|
|
475
|
-
###
|
|
475
|
+
### Wrap layout
|
|
476
476
|
|
|
477
|
-
`Layout.
|
|
478
|
-
column; each keeps the width implied by its content or
|
|
479
|
-
out left to right, wrapping to the next row whenever the
|
|
480
|
-
fits in the remaining frame width. Widgets in the same
|
|
481
|
-
centered on their midline, so a tall `entry` aligns cleanly
|
|
482
|
-
`button` or `checkbutton`. The gap inherits the layout spacing
|
|
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).
|
|
483
484
|
|
|
484
485
|
```python
|
|
485
486
|
TAGS = ["python", "tkinter", "async", "uv", "type hints"]
|
|
@@ -488,13 +489,46 @@ for tag in TAGS:
|
|
|
488
489
|
def on_tag(values, tag=tag):
|
|
489
490
|
return {"msg": f"Selected: {tag}"}
|
|
490
491
|
|
|
491
|
-
app.run(layout=Layout(spacing=2).status("msg").
|
|
492
|
+
app.run(layout=Layout(spacing=2).status("msg").wrap(*TAGS))
|
|
492
493
|
```
|
|
493
494
|
|
|
494
|
-
|
|
495
|
-
rows automatically when the window is resized. Pass `
|
|
496
|
-
spacing default, and `side`/`fill`/`expand` to control how the
|
|
497
|
-
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.
|
|
498
532
|
|
|
499
533
|
### Dynamic region switching (swap targets)
|
|
500
534
|
|
|
@@ -503,7 +537,7 @@ HTMX `hx-target` / `hx-swap`. Surrounding sections (toolbar, status) stay fixed
|
|
|
503
537
|
while only the target region swaps:
|
|
504
538
|
|
|
505
539
|
```python
|
|
506
|
-
layout = Layout().
|
|
540
|
+
layout = Layout().wrap("go_dir", "go_file", "info").target("main_area")
|
|
507
541
|
|
|
508
542
|
@app.swap(
|
|
509
543
|
"main_area",
|
|
@@ -750,6 +784,18 @@ print(app.debug_layout())
|
|
|
750
784
|
# → {"title": "...", "sections": [{"widgets": [{"name": "msg", "geometry": ..., ...}, ...]}]}
|
|
751
785
|
```
|
|
752
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
|
+
|
|
753
799
|
---
|
|
754
800
|
|
|
755
801
|
## Async-Native (asyncio + Tkinter)
|
|
@@ -782,6 +828,7 @@ uv run python examples/filepicker_demo.py # file picker
|
|
|
782
828
|
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
783
829
|
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
784
830
|
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
831
|
+
uv run python examples/bottom_bar_demo.py # pinned bottom bar (section side="bottom")
|
|
785
832
|
```
|
|
786
833
|
|
|
787
834
|
---
|
|
@@ -210,7 +210,96 @@ PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
|
210
210
|
|
|
211
211
|
---
|
|
212
212
|
|
|
213
|
-
##
|
|
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
|
+
## Near term (post-0.4.5)
|
|
214
303
|
|
|
215
304
|
### A11y
|
|
216
305
|
|
|
@@ -240,6 +329,9 @@ PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
|
240
329
|
- [x] Nested frames (`Layout` inside `Layout`) via `Layout.frame(...)`
|
|
241
330
|
- [x] Global default for `padx`/`pady` via `Layout(spacing=...)`
|
|
242
331
|
- [x] `Layout.cluster(...)` wrapping flow; `Layout.paired(..., sync_yscroll=)`
|
|
332
|
+
- [x] `Layout.cluster(...)` → `Layout.wrap(...)` rename with deprecated alias (v0.4.5)
|
|
333
|
+
- [x] `Flex` (Flutter `Expanded` analog) for `wrap` (v0.4.5)
|
|
334
|
+
- [x] `FlowDelegate` / `Layout.flow` custom flow (Flutter `Flow` analog) (v0.4.5)
|
|
243
335
|
- [x] `Layout.paired(..., line_numbers=True)` read-only line-number gutters (v0.4.4)
|
|
244
336
|
- [x] `Layout.target(...)` + `@app.swap(...)` dynamic region switching (v0.4.4)
|
|
245
337
|
|
|
@@ -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",
|
|
@@ -904,6 +904,7 @@ class TkApp:
|
|
|
904
904
|
sec_id = id(master)
|
|
905
905
|
if sec_id not in seen_sections:
|
|
906
906
|
seen_sections[sec_id] = {
|
|
907
|
+
"master": master,
|
|
907
908
|
"master_class": master.winfo_class(),
|
|
908
909
|
"master_geometry": master.winfo_geometry(),
|
|
909
910
|
"widgets": [],
|
|
@@ -939,8 +940,76 @@ class TkApp:
|
|
|
939
940
|
pass
|
|
940
941
|
seen_sections[sec_id]["widgets"].append(info)
|
|
941
942
|
out["sections"] = list(seen_sections.values())
|
|
943
|
+
|
|
944
|
+
# Detect geometry-manager conflicts. Tk forbids mixing pack/grid/place
|
|
945
|
+
# on the same master; a registered widget's master may also host
|
|
946
|
+
# manual widgets (e.g. an overlay placed with ``place``), which only
|
|
947
|
+
# surface by inspecting the live child tree via ``winfo_manager``.
|
|
948
|
+
conflicts: list[dict[str, Any]] = []
|
|
949
|
+
for sec in out["sections"]:
|
|
950
|
+
master = sec.get("master")
|
|
951
|
+
managers: set[str] = set()
|
|
952
|
+
widget_names: list[str] = []
|
|
953
|
+
for w in sec["widgets"]:
|
|
954
|
+
if w["manager"]:
|
|
955
|
+
managers.add(w["manager"])
|
|
956
|
+
widget_names.append(w["name"])
|
|
957
|
+
if master is not None:
|
|
958
|
+
try:
|
|
959
|
+
for child in master.winfo_children():
|
|
960
|
+
mgr = child.winfo_manager()
|
|
961
|
+
if mgr:
|
|
962
|
+
managers.add(mgr)
|
|
963
|
+
label = child.cget("text") if child.winfo_class() in (
|
|
964
|
+
"Label", "TLabel"
|
|
965
|
+
) else child.winfo_class()
|
|
966
|
+
widget_names.append(label or child.winfo_name())
|
|
967
|
+
except tk.TclError:
|
|
968
|
+
pass
|
|
969
|
+
if len(managers) > 1:
|
|
970
|
+
conflicts.append({
|
|
971
|
+
"master_class": sec["master_class"],
|
|
972
|
+
"managers": sorted(managers),
|
|
973
|
+
"widgets": widget_names,
|
|
974
|
+
})
|
|
975
|
+
sec.pop("master", None)
|
|
976
|
+
out["conflicts"] = conflicts
|
|
942
977
|
return out
|
|
943
978
|
|
|
979
|
+
def check_layout_conflicts(self) -> list[dict[str, Any]]:
|
|
980
|
+
"""Return a list of geometry-manager conflicts, warning on each.
|
|
981
|
+
|
|
982
|
+
Tcl/Tk raises ``TclError: conflicting geometry managers`` at runtime
|
|
983
|
+
when a widget is managed by a different manager than its siblings on
|
|
984
|
+
the same master. This helper surfaces the same condition without
|
|
985
|
+
waiting for a failure: it inspects the live widget tree and reports
|
|
986
|
+
any master whose children are split across multiple managers
|
|
987
|
+
(``pack``/``grid``/``place``).
|
|
988
|
+
|
|
989
|
+
Each returned item describes one conflicting master::
|
|
990
|
+
|
|
991
|
+
{
|
|
992
|
+
"master_class": "Frame",
|
|
993
|
+
"managers": ["grid", "pack"],
|
|
994
|
+
"widgets": ["left_pane", "location"],
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
A ``warnings.warn`` is emitted for every conflict found, so it can be
|
|
998
|
+
used as a debugging aid after ``build_widgets`` (or inside ``run``).
|
|
999
|
+
"""
|
|
1000
|
+
debug = self.debug_layout()
|
|
1001
|
+
conflicts: list[dict[str, Any]] = []
|
|
1002
|
+
for conflict in debug.get("conflicts", []):
|
|
1003
|
+
conflicts.append(conflict)
|
|
1004
|
+
warnings.warn(
|
|
1005
|
+
"Conflicting geometry managers on same master: "
|
|
1006
|
+
f"{conflict['managers']} for widgets "
|
|
1007
|
+
f"{conflict['widgets']} ({conflict['master_class']}). "
|
|
1008
|
+
"Pack/grid/place cannot be mixed on one widget.",
|
|
1009
|
+
stacklevel=2,
|
|
1010
|
+
)
|
|
1011
|
+
return conflicts
|
|
1012
|
+
|
|
944
1013
|
def _spec(self, name: str) -> WidgetSpec | None:
|
|
945
1014
|
for w in self._widgets:
|
|
946
1015
|
if w.name == name:
|