wx-accessible-grid 0.1.0__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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .DS_Store
8
+ .ruff_cache/
@@ -0,0 +1,42 @@
1
+ # Contributing
2
+
3
+ **Contributions are welcome — anyone can contribute, and we want them.** This is
4
+ a Community Access open-source project, created by Taylor Arndt.
5
+
6
+ ## Ways to help
7
+ - **Report bugs** — open an issue with what you expected, what happened, your OS,
8
+ wxPython version, whether you use a webview, and your screen reader.
9
+ - **Test with screen readers** — NVDA, JAWS, Narrator (Windows), VoiceOver
10
+ (macOS), Orca (Linux). Real-world a11y reports are the most valuable thing here.
11
+ Tell us how arrow navigation, in-cell editing, and selection announced.
12
+ - **Send pull requests** — features, fixes, docs, examples.
13
+
14
+ ## Ground rules
15
+ - **Accessibility first.** The whole point is a data grid that is fully
16
+ keyboard-operable and reads correctly in NVDA and JAWS. The grid must speak the
17
+ column header as you move across a row, the row header as you move down a
18
+ column, and only the focused cell (never the whole table on every arrow).
19
+ Changes shouldn't regress that; note how you tested.
20
+ - **Real ARIA grid, never a div soup.** We render a semantic `<table role="grid">`
21
+ so the screen reader gets header association and row/column counts for free. A
22
+ hand-drawn grid reads worse.
23
+ - **The model owns the truth.** The grid never mutates data directly — it asks the
24
+ model and shows what the model says is now true, so an edit the user hears
25
+ confirmed is the validated, normalized value.
26
+ - Keep it **dependency-light** — wxPython plus its sibling `wx-accessible-webview`.
27
+ - Match the existing style; format with `ruff format` (line length 100).
28
+
29
+ ## Dev setup
30
+ ```bash
31
+ pip install -e ".[dev]"
32
+ python examples/demo.py # try it with a screen reader running
33
+ pytest # the model and renderer are tested without wx
34
+ ```
35
+
36
+ ## Pull requests
37
+ - Describe the change and how you verified it (which screen reader / OS, native
38
+ or webview).
39
+ - One focused change per PR is easiest to review.
40
+
41
+ Thanks for helping make accessible data grids in wxPython the default, not the
42
+ exception.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Taylor Arndt and Community Access
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: wx-accessible-grid
3
+ Version: 0.1.0
4
+ Summary: An accessible, editable data grid for wxPython rendered as a real ARIA grid in a WebView (arrow-key cell navigation, in-cell editing, row selection, context menus) that reads correctly in NVDA and JAWS.
5
+ Project-URL: Homepage, https://github.com/Community-Access/wx-accessible-grid
6
+ Author: Community Access
7
+ Author-email: Taylor Arndt <taylor@techopolisonline.com>
8
+ Maintainer-email: Taylor Arndt <taylor@techopolisonline.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: a11y,accessibility,aria,datagrid,grid,jaws,keyboard,nvda,screen-reader,table,webview,webview2,wxpython,wxwidgets
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Adaptive Technologies
17
+ Classifier: Topic :: Software Development :: User Interfaces
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: wx-accessible-webview>=0.2.0
20
+ Requires-Dist: wxpython>=4.2.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # wx-accessible-grid
26
+
27
+ An accessible, editable data grid for wxPython that a blind person can actually
28
+ use. Native `wx.grid.Grid` reads poorly or not at all in NVDA and JAWS, and a
29
+ hand-built grid of `<div>`s is worse. This library takes the approach proven by
30
+ its sibling [wx-accessible-webview](https://github.com/Community-Access/wx-accessible-webview):
31
+ render a real, semantic ARIA grid into a WebView and let the screen reader follow
32
+ it like any web data table, then layer spreadsheet-style keyboard behavior on top.
33
+
34
+ It is built for data entry, not a spreadsheet engine. There are no formulas. What
35
+ there is, is every control you actually edit data with, fully keyboard-operable
36
+ and announced correctly.
37
+
38
+ ## What you get
39
+
40
+ - Arrow keys move a single focused cell, not the document. Moving across a row
41
+ speaks the column header. Moving down a column speaks the row header. Only the
42
+ focused cell is read, so a grid with thousands of rows stays fast instead of
43
+ making the screen reader re-read the whole table on every keystroke.
44
+ - `F2` or `Enter` edits a cell in place with the right control for the data: edit
45
+ box, combo box, checkbox, slider, or stepper. `Enter` commits, `Escape` cancels.
46
+ - `Space`, or `Ctrl+Space`, selects a row. `Delete` deletes the selection. The
47
+ context menu key, or `Shift+F10`, fires a callback so you can show a native menu.
48
+ - Pass `row_select=True` to add a real checkbox at the start of every row, so
49
+ selecting rows for a bulk operation (move, reorder, edit a region) is visible
50
+ and discoverable, not just a keystroke. The host reads `grid.selected_rows()`,
51
+ acts through the model, and calls `grid.refresh()`.
52
+ - Editing round-trips through your model, so the value the screen reader confirms
53
+ is the validated, normalized one, never the raw keystrokes. If an edit is
54
+ rejected the user hears why and the editor reopens so they can fix it.
55
+ - Only one page of rows is ever in the DOM, but `aria-rowcount` keeps the user's
56
+ sense of "row N of many" correct. Arrow past the bottom and the next page loads
57
+ and navigation just keeps going.
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install wx-accessible-grid
63
+ ```
64
+
65
+ That pulls in wxPython and wx-accessible-webview.
66
+
67
+ ## Use it
68
+
69
+ Subclass `GridModel` to describe your columns and provide the data, then drop an
70
+ `AccessibleGrid` into a sizer.
71
+
72
+ ```python
73
+ import wx
74
+ from wx_accessible_grid import AccessibleGrid, GridModel, Column, SetResult
75
+ from wx_accessible_grid import TEXT, COMBO, CHECKBOX, SLIDER, STEPPER, NONE
76
+
77
+ class ChannelModel(GridModel):
78
+ def __init__(self, rows):
79
+ self._rows = rows
80
+ self._cols = [
81
+ Column("num", "#", editor=NONE, is_row_header=True),
82
+ Column("name", "Name", editor=TEXT),
83
+ Column("mode", "Mode", editor=COMBO, choices=["FM", "AM", "USB"]),
84
+ Column("active", "Active", editor=CHECKBOX),
85
+ Column("volume", "Volume", editor=SLIDER, min=0, max=100, step=1),
86
+ Column("priority", "Priority", editor=STEPPER, min=0, max=10),
87
+ ]
88
+
89
+ def columns(self):
90
+ return self._cols
91
+
92
+ def row_count(self):
93
+ return len(self._rows)
94
+
95
+ def display(self, row, column):
96
+ if column == "num":
97
+ return str(row + 1)
98
+ val = self._rows[row][column]
99
+ return "Yes" if (column == "active" and val) else "No" if column == "active" else str(val)
100
+
101
+ def edit_value(self, row, column):
102
+ # the form the editor wants, when it differs from the shown text
103
+ if column == "active":
104
+ return "true" if self._rows[row]["active"] else "false"
105
+ return self.display(row, column)
106
+
107
+ def set_cell(self, row, column, value):
108
+ # validate and normalize here; what you return is what gets announced
109
+ self._rows[row][column] = value
110
+ return SetResult(True, display=value, message=f"{column} updated")
111
+
112
+ grid = AccessibleGrid(panel, ChannelModel(rows), label="Memory channels",
113
+ page_size=100)
114
+ sizer.Add(grid.control, 1, wx.EXPAND)
115
+ ```
116
+
117
+ The `dev` extra (`pip install "wx-accessible-grid[dev]"`) adds pytest for the
118
+ model and renderer tests, which run without wx.
119
+
120
+ ## The editors
121
+
122
+ - `TEXT`: a single-line edit box. Type, then Enter to commit.
123
+ - `COMBO`: a drop-down of fixed choices, which also stands in for a radio group.
124
+ Arrow through the list, Enter to commit.
125
+ - `CHECKBOX`: a boolean toggle. Space toggles it, Enter commits.
126
+ - `SLIDER`: a range control adjusted with the arrow keys.
127
+ - `STEPPER`: a number spinner. Type a value or step it with the arrows.
128
+ - `NONE`: read-only, for ids and computed columns. Not editable.
129
+
130
+ `edit_value` gives the editor its starting value when that differs from the
131
+ displayed text (a checkbox wants `true`/`false`, a slider wants a bare number,
132
+ even if the cell shows `Yes` or `146.520 MHz`).
133
+
134
+ ## Keyboard
135
+
136
+ - Arrows: move the focused cell. `Home` / `End`: first / last cell in the row.
137
+ `Ctrl+Home` / `Ctrl+End`: first / last cell in the grid. `Page Up` / `Page Down`:
138
+ previous / next page.
139
+ - `F2` or `Enter`: edit the focused cell. `Enter`: commit. `Escape`: cancel.
140
+ - `Space` or `Ctrl+Space`: select or unselect the row.
141
+ - `Delete`: delete the selected rows, or the focused row if none are selected.
142
+ - Context menu key or `Shift+F10`: ask the host for a row menu.
143
+
144
+ ## How it works
145
+
146
+ The grid is a `<table role="grid">` with `<th scope="col">` column headers,
147
+ a `<th scope="row">` row header, and `<td role="gridcell">` cells, rendered into
148
+ an `AccessibleWebView`. A roving `tabindex` gives exactly one cell focus at a
149
+ time, which puts the screen reader in focus mode so it reads that cell and the
150
+ headers that changed, not the whole table. A small vanilla-JS runtime, installed
151
+ once, handles navigation, editing, and selection, and talks to Python over the
152
+ WebView bridge. Your `GridModel` does the validation and persistence on the
153
+ Python side.
154
+
155
+ ## Status
156
+
157
+ Version 0.1.0, first release, built for VRP (the accessible radio programmer) and
158
+ extracted as a reusable library. Tested on macOS WebView with VoiceOver and in
159
+ unit tests for the model and renderer. NVDA and JAWS verification on Windows
160
+ WebView2 is the next milestone; reports welcome.
161
+
162
+ ## License
163
+
164
+ MIT. A Community Access open-source project, created by Taylor Arndt.
@@ -0,0 +1,140 @@
1
+ # wx-accessible-grid
2
+
3
+ An accessible, editable data grid for wxPython that a blind person can actually
4
+ use. Native `wx.grid.Grid` reads poorly or not at all in NVDA and JAWS, and a
5
+ hand-built grid of `<div>`s is worse. This library takes the approach proven by
6
+ its sibling [wx-accessible-webview](https://github.com/Community-Access/wx-accessible-webview):
7
+ render a real, semantic ARIA grid into a WebView and let the screen reader follow
8
+ it like any web data table, then layer spreadsheet-style keyboard behavior on top.
9
+
10
+ It is built for data entry, not a spreadsheet engine. There are no formulas. What
11
+ there is, is every control you actually edit data with, fully keyboard-operable
12
+ and announced correctly.
13
+
14
+ ## What you get
15
+
16
+ - Arrow keys move a single focused cell, not the document. Moving across a row
17
+ speaks the column header. Moving down a column speaks the row header. Only the
18
+ focused cell is read, so a grid with thousands of rows stays fast instead of
19
+ making the screen reader re-read the whole table on every keystroke.
20
+ - `F2` or `Enter` edits a cell in place with the right control for the data: edit
21
+ box, combo box, checkbox, slider, or stepper. `Enter` commits, `Escape` cancels.
22
+ - `Space`, or `Ctrl+Space`, selects a row. `Delete` deletes the selection. The
23
+ context menu key, or `Shift+F10`, fires a callback so you can show a native menu.
24
+ - Pass `row_select=True` to add a real checkbox at the start of every row, so
25
+ selecting rows for a bulk operation (move, reorder, edit a region) is visible
26
+ and discoverable, not just a keystroke. The host reads `grid.selected_rows()`,
27
+ acts through the model, and calls `grid.refresh()`.
28
+ - Editing round-trips through your model, so the value the screen reader confirms
29
+ is the validated, normalized one, never the raw keystrokes. If an edit is
30
+ rejected the user hears why and the editor reopens so they can fix it.
31
+ - Only one page of rows is ever in the DOM, but `aria-rowcount` keeps the user's
32
+ sense of "row N of many" correct. Arrow past the bottom and the next page loads
33
+ and navigation just keeps going.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install wx-accessible-grid
39
+ ```
40
+
41
+ That pulls in wxPython and wx-accessible-webview.
42
+
43
+ ## Use it
44
+
45
+ Subclass `GridModel` to describe your columns and provide the data, then drop an
46
+ `AccessibleGrid` into a sizer.
47
+
48
+ ```python
49
+ import wx
50
+ from wx_accessible_grid import AccessibleGrid, GridModel, Column, SetResult
51
+ from wx_accessible_grid import TEXT, COMBO, CHECKBOX, SLIDER, STEPPER, NONE
52
+
53
+ class ChannelModel(GridModel):
54
+ def __init__(self, rows):
55
+ self._rows = rows
56
+ self._cols = [
57
+ Column("num", "#", editor=NONE, is_row_header=True),
58
+ Column("name", "Name", editor=TEXT),
59
+ Column("mode", "Mode", editor=COMBO, choices=["FM", "AM", "USB"]),
60
+ Column("active", "Active", editor=CHECKBOX),
61
+ Column("volume", "Volume", editor=SLIDER, min=0, max=100, step=1),
62
+ Column("priority", "Priority", editor=STEPPER, min=0, max=10),
63
+ ]
64
+
65
+ def columns(self):
66
+ return self._cols
67
+
68
+ def row_count(self):
69
+ return len(self._rows)
70
+
71
+ def display(self, row, column):
72
+ if column == "num":
73
+ return str(row + 1)
74
+ val = self._rows[row][column]
75
+ return "Yes" if (column == "active" and val) else "No" if column == "active" else str(val)
76
+
77
+ def edit_value(self, row, column):
78
+ # the form the editor wants, when it differs from the shown text
79
+ if column == "active":
80
+ return "true" if self._rows[row]["active"] else "false"
81
+ return self.display(row, column)
82
+
83
+ def set_cell(self, row, column, value):
84
+ # validate and normalize here; what you return is what gets announced
85
+ self._rows[row][column] = value
86
+ return SetResult(True, display=value, message=f"{column} updated")
87
+
88
+ grid = AccessibleGrid(panel, ChannelModel(rows), label="Memory channels",
89
+ page_size=100)
90
+ sizer.Add(grid.control, 1, wx.EXPAND)
91
+ ```
92
+
93
+ The `dev` extra (`pip install "wx-accessible-grid[dev]"`) adds pytest for the
94
+ model and renderer tests, which run without wx.
95
+
96
+ ## The editors
97
+
98
+ - `TEXT`: a single-line edit box. Type, then Enter to commit.
99
+ - `COMBO`: a drop-down of fixed choices, which also stands in for a radio group.
100
+ Arrow through the list, Enter to commit.
101
+ - `CHECKBOX`: a boolean toggle. Space toggles it, Enter commits.
102
+ - `SLIDER`: a range control adjusted with the arrow keys.
103
+ - `STEPPER`: a number spinner. Type a value or step it with the arrows.
104
+ - `NONE`: read-only, for ids and computed columns. Not editable.
105
+
106
+ `edit_value` gives the editor its starting value when that differs from the
107
+ displayed text (a checkbox wants `true`/`false`, a slider wants a bare number,
108
+ even if the cell shows `Yes` or `146.520 MHz`).
109
+
110
+ ## Keyboard
111
+
112
+ - Arrows: move the focused cell. `Home` / `End`: first / last cell in the row.
113
+ `Ctrl+Home` / `Ctrl+End`: first / last cell in the grid. `Page Up` / `Page Down`:
114
+ previous / next page.
115
+ - `F2` or `Enter`: edit the focused cell. `Enter`: commit. `Escape`: cancel.
116
+ - `Space` or `Ctrl+Space`: select or unselect the row.
117
+ - `Delete`: delete the selected rows, or the focused row if none are selected.
118
+ - Context menu key or `Shift+F10`: ask the host for a row menu.
119
+
120
+ ## How it works
121
+
122
+ The grid is a `<table role="grid">` with `<th scope="col">` column headers,
123
+ a `<th scope="row">` row header, and `<td role="gridcell">` cells, rendered into
124
+ an `AccessibleWebView`. A roving `tabindex` gives exactly one cell focus at a
125
+ time, which puts the screen reader in focus mode so it reads that cell and the
126
+ headers that changed, not the whole table. A small vanilla-JS runtime, installed
127
+ once, handles navigation, editing, and selection, and talks to Python over the
128
+ WebView bridge. Your `GridModel` does the validation and persistence on the
129
+ Python side.
130
+
131
+ ## Status
132
+
133
+ Version 0.1.0, first release, built for VRP (the accessible radio programmer) and
134
+ extracted as a reusable library. Tested on macOS WebView with VoiceOver and in
135
+ unit tests for the model and renderer. NVDA and JAWS verification on Windows
136
+ WebView2 is the next milestone; reports welcome.
137
+
138
+ ## License
139
+
140
+ MIT. A Community Access open-source project, created by Taylor Arndt.
@@ -0,0 +1,101 @@
1
+ # Agent notes — wx-accessible-grid
2
+
3
+ Handoff log. Read this first; update it as you go.
4
+
5
+ ## What this is
6
+
7
+ A reusable, accessible, editable data grid for wxPython, spun out the same way as
8
+ its siblings `wx-accessible-webview` and `wx-accessible-menubar`. Community Access
9
+ open-source, MIT, created by Taylor Arndt. First consumer is VRP (the accessible
10
+ radio programmer), whose channel grid is the motivating use case.
11
+
12
+ The hard problem it solves: native `wx.grid.Grid` is inaccessible, and earlier
13
+ in-grid editing attempts in VRP made NVDA re-read the whole table on every
14
+ keystroke (so they fell back to a read-only table plus a native edit dialog).
15
+ The fix here is a real `<table role="grid">` with a roving tabindex, so NVDA goes
16
+ into focus mode and reads only the focused cell plus the headers that changed.
17
+
18
+ ## Architecture
19
+
20
+ - `model.py` — `GridModel` (subclass it), `Column` (with one of five editors:
21
+ text, combo, checkbox, slider, stepper, plus `none`), `SetResult`. The model
22
+ owns all data, validation, and persistence; the grid never mutates data
23
+ directly. wx-free, unit tested.
24
+ - `render.py` — renders the model to ARIA grid HTML one page at a time.
25
+ `aria-rowcount` = total + 1 (header), `aria-rowindex` absolute (header = 1,
26
+ first data row = 2). wx-free, unit tested.
27
+ - `assets.py` — `GRID_CSS` and the vanilla-JS runtime (`runtime_js(handler)`).
28
+ The runtime is delegated off `document` so it survives re-renders; it manages
29
+ the roving tabindex, navigation, selection, the five in-cell editors, and the
30
+ Python bridge. `window.__wag` exposes setColumns/setCell/editFailed/removeRow/
31
+ announce/focusCell for Python to call back.
32
+ - `grid.py` — `AccessibleGrid` widget. Owns an `AccessibleWebView`, installs the
33
+ runtime on load (EVT_WEBVIEW_LOADED + CallAfter), pushes column metadata,
34
+ renders pages, and brokers edit/select/delete/page/context bridge messages to
35
+ the model. `grid.control` goes in a sizer.
36
+ - `examples/demo.py` — standalone 2,500-row demo with all five editors (run it
37
+ with NVDA on Windows). `tests/` — model + renderer tests (no wx needed).
38
+
39
+ ## Bridge protocol
40
+
41
+ Page to Python (all `{type:"wag", action, ...}`): `edit{row,col,value}`,
42
+ `select{row,selected,count}`, `delete{rows}`, `page{dir,col,fromRow}`,
43
+ `context{row,col}`. `col` is the 0-based column index; `row` is the absolute
44
+ 0-based row index.
45
+
46
+ ## Focus model (important — read before editing assets.py/render.py)
47
+
48
+ The grid uses the **aria-activedescendant** pattern, NOT roving tabindex. The
49
+ `<table>` is the only focusable element (`tabindex=0`) and owns
50
+ `aria-activedescendant`; the runtime moves that to the active cell's id instead
51
+ of calling `cell.focus()`. This is deliberate: the accessibility-lead review
52
+ found a focused `role=gridcell` with roving tabindex does NOT reliably put NVDA
53
+ in focus mode under WebView2 (arrows would be dead), and every innerHTML swap
54
+ bounced focus through document.body and out of focus mode. activedescendant fixes
55
+ both. Do not "simplify" back to roving tabindex + cell.focus().
56
+
57
+ Paging and delete replace ONLY the tbody (`window.__wag.setRows`) so the table
58
+ element — and thus focus + focus mode — survives. Full `set_content` is only used
59
+ for the initial render and `refresh()`.
60
+
61
+ ## Status (2026-06-20)
62
+
63
+ - Built v0.1.0. accessibility-lead review done (4 specialists). It was a NO-SHIP;
64
+ all three Criticals fixed: (1) migrated to aria-activedescendant; (2) edit/
65
+ delete/page no longer drop focus to body; (3) two real aria-live regions are
66
+ rendered (polite + assertive) and success edits now announce the authoritative
67
+ value. Should-fix items also applied: Tab/blur edit trap, read-only announce on
68
+ F2, header-row navigation, page-edge guards, cell-level aria-selected,
69
+ aria-invalid + message-in-name on reject, identical-text re-announce. (A few
70
+ nice-to-haves from the review remain, noted in the review output.)
71
+ - 11 unit tests pass (`PYTHONPATH=src pytest`). Mac WKWebView smoke passes:
72
+ build, edit (incl. reject keeps old value), paging, delete, runtime install.
73
+ - WIRED INTO VRP and tested against a real CHIRP image (Baofeng UV-5R, 128
74
+ channels): all real columns render, name edit persisted (and the UV-5R's name
75
+ truncation came back as the authoritative display — the round-trip works), bad
76
+ frequency rejected. VRP files: `vrp/channel_grid_model.py` (ChannelGridModel),
77
+ `tools/grid_preview.py` (launcher), dep added to VRP pyproject + installed
78
+ editable. VRP's own 63 tests still pass.
79
+ - NOT yet verified on Windows + NVDA/WebView2 — the make-or-break test, next
80
+ milestone. On the Parallels VM: `uv run python tools/grid_preview.py` in the
81
+ VRP clone (see windows-parallels-prlctl memory). The review's step-by-step VM
82
+ test script is in that review output; step 1 (Tab in, press Down, expect a
83
+ sibling cell, not a document line) is the focus-mode gate.
84
+ - VRP's read-only table + native edit dialog are left in place; the grid is a
85
+ preview/beta path until NVDA proves out, then it replaces the channels view
86
+ (keep the dialog as the full-row fallback).
87
+ - No git commits yet (Taylor commits/pushes himself). No PyPI release yet.
88
+
89
+ ## Testing
90
+
91
+ - `PYTHONPATH=src <python-with-wx> -m pytest -q` — model + renderer.
92
+ - `pip install -e ".[dev]"` then `pytest` in a clean env.
93
+ - Real test is manual: `python examples/demo.py` with a screen reader. Drive the
94
+ arrow navigation, F2/Enter editing for each editor, Space selection, Delete,
95
+ and the context-menu key, and confirm the announcements.
96
+
97
+ ## Conventions
98
+
99
+ Mirror the sibling libs: hatchling build, `src/` layout, MIT, ruff line length
100
+ 100, dependency-light (wxPython + wx-accessible-webview). No emojis, no markdown
101
+ tables in docs (screen-reader rule).
@@ -0,0 +1,154 @@
1
+ """Standalone demo of AccessibleGrid — run it with a screen reader.
2
+
3
+ pip install -e .
4
+ python examples/demo.py
5
+
6
+ It builds a 2,500-row grid (to exercise paging and large-dataset performance,
7
+ the exact condition that broke earlier in-grid attempts) with one of every
8
+ editor: text, combo, checkbox, slider, stepper. Try, with NVDA running:
9
+
10
+ * Arrow around — moving across a row speaks the column; moving down speaks the
11
+ row number. Arrow past the bottom/top to roll onto the next/previous page.
12
+ * F2 or Enter on a cell to edit; Enter commits, Escape cancels.
13
+ * Space (or Ctrl+Space) to select a row; Delete to delete selected rows.
14
+ * The Applications key (or Shift+F10) on a cell opens a native row menu.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import wx
20
+
21
+ from wx_accessible_grid import (
22
+ CHECKBOX,
23
+ COMBO,
24
+ NONE,
25
+ SLIDER,
26
+ STEPPER,
27
+ TEXT,
28
+ AccessibleGrid,
29
+ Column,
30
+ GridModel,
31
+ SetResult,
32
+ )
33
+
34
+ MODES = ["FM", "NFM", "AM", "USB", "LSB", "CW", "DV"]
35
+
36
+
37
+ class DemoModel(GridModel):
38
+ """An in-memory grid: a list of dict rows, all validation inline."""
39
+
40
+ def __init__(self, n: int = 2500) -> None:
41
+ self._cols = [
42
+ Column("num", "#", editor=NONE, is_row_header=True),
43
+ Column("name", "Name", editor=TEXT),
44
+ Column("mode", "Mode", editor=COMBO, choices=MODES),
45
+ Column("active", "Active", editor=CHECKBOX),
46
+ Column("volume", "Volume", editor=SLIDER, min=0, max=100, step=1),
47
+ Column("priority", "Priority", editor=STEPPER, min=0, max=10, step=1),
48
+ Column("comment", "Comment", editor=TEXT),
49
+ ]
50
+ self._rows = [
51
+ {
52
+ "name": f"Channel {i + 1}",
53
+ "mode": MODES[i % len(MODES)],
54
+ "active": i % 3 == 0,
55
+ "volume": (i * 7) % 101,
56
+ "priority": i % 11,
57
+ "comment": "",
58
+ }
59
+ for i in range(n)
60
+ ]
61
+
62
+ def columns(self) -> list[Column]:
63
+ return self._cols
64
+
65
+ def row_count(self) -> int:
66
+ return len(self._rows)
67
+
68
+ def display(self, row: int, column: str) -> str:
69
+ if column == "num":
70
+ return str(row + 1)
71
+ val = self._rows[row][column]
72
+ if column == "active":
73
+ return "Yes" if val else "No"
74
+ return str(val)
75
+
76
+ def edit_value(self, row: int, column: str) -> str:
77
+ if column == "active":
78
+ return "true" if self._rows[row]["active"] else "false"
79
+ if column == "num":
80
+ return str(row + 1)
81
+ return str(self._rows[row][column])
82
+
83
+ def set_cell(self, row: int, column: str, value: str) -> SetResult:
84
+ if column in ("volume", "priority"):
85
+ try:
86
+ num = int(float(value))
87
+ except ValueError:
88
+ return SetResult(False, message=f"{column} must be a number")
89
+ lo, hi = (0, 100) if column == "volume" else (0, 10)
90
+ if not lo <= num <= hi:
91
+ return SetResult(False, message=f"{column} must be {lo} to {hi}")
92
+ self._rows[row][column] = num
93
+ return SetResult(True, str(num), f"{column} set to {num}")
94
+ if column == "active":
95
+ on = value in ("true", "1", "yes")
96
+ self._rows[row]["active"] = on
97
+ return SetResult(True, "Yes" if on else "No", "Active on" if on else "Active off")
98
+ if column == "mode":
99
+ if value not in MODES:
100
+ return SetResult(False, message=f"{value} is not a valid mode")
101
+ self._rows[row]["mode"] = value
102
+ return SetResult(True, value, f"Mode {value}")
103
+ self._rows[row][column] = value
104
+ return SetResult(True, value, f"{column} updated")
105
+
106
+ def delete_rows(self, rows: list[int]) -> SetResult:
107
+ for r in sorted(rows, reverse=True):
108
+ if 0 <= r < len(self._rows):
109
+ del self._rows[r]
110
+ return SetResult(True, message=f"Deleted {len(rows)} row(s)")
111
+
112
+
113
+ class DemoFrame(wx.Frame):
114
+ def __init__(self) -> None:
115
+ super().__init__(None, title="wx-accessible-grid demo", size=(900, 600))
116
+ panel = wx.Panel(self)
117
+ self.model = DemoModel()
118
+ self.grid = AccessibleGrid(
119
+ panel,
120
+ self.model,
121
+ label="Demo channels",
122
+ page_size=100,
123
+ row_select=True,
124
+ on_context=self._on_context,
125
+ description="Arrow to move, F2 or Enter to edit, Space to select.",
126
+ )
127
+ sizer = wx.BoxSizer(wx.VERTICAL)
128
+ sizer.Add(self.grid.control, 1, wx.EXPAND)
129
+ panel.SetSizer(sizer)
130
+ self.Show()
131
+ wx.CallAfter(self.grid.focus)
132
+
133
+ def _on_context(self, row: int, column: str) -> None:
134
+ menu = wx.Menu()
135
+ edit = menu.Append(wx.ID_ANY, f"Edit row {row + 1} (full)\tEnter")
136
+ delete = menu.Append(wx.ID_ANY, f"Delete row {row + 1}\tDel")
137
+ self.Bind(
138
+ wx.EVT_MENU,
139
+ lambda _e: self.grid.announce(f"Would open full editor for row {row + 1}"),
140
+ edit,
141
+ )
142
+ self.Bind(
143
+ wx.EVT_MENU,
144
+ lambda _e: (self.model.delete_rows([row]), self.grid.refresh()),
145
+ delete,
146
+ )
147
+ self.grid.control.PopupMenu(menu)
148
+ menu.Destroy()
149
+
150
+
151
+ if __name__ == "__main__":
152
+ app = wx.App()
153
+ DemoFrame()
154
+ app.MainLoop()