Skip to content

Migrate GUI from tkinter to wxPython for VoiceOver accessibility#1

Open
payown wants to merge 9 commits intoacbnational:mainfrom
payown:fix/add-build-system
Open

Migrate GUI from tkinter to wxPython for VoiceOver accessibility#1
payown wants to merge 9 commits intoacbnational:mainfrom
payown:fix/add-build-system

Conversation

@payown
Copy link

@payown payown commented Feb 17, 2026

Summary

  • Replaces tkinter with wxPython across the entire GUI layer to fix a critical accessibility gap: tkinter's Tcl/Tk backend does not implement Apple's NSAccessibility protocol, making Settings and Status windows completely invisible to VoiceOver on macOS
  • wxPython uses native Cocoa widgets on macOS and native Win32 on Windows, so all controls automatically participate in the OS accessibility hierarchy — fixing VoiceOver support and improving JAWS/NVDA on Windows
  • All controls now have explicit .SetName() accessible labels so screen readers announce them correctly

What changed

File Change
acb_sync/ui.py Full rewrite — all tkinter widgets/layouts/events become wxPython (wx.Dialog, wx.Frame, wx.ScrolledWindow, wx.ListCtrl, wx.StaticBoxSizer, wx.FlexGridSizer, wx.Timer, wx.EVT_CHAR_HOOK for hotkey capture)
acb_sync/app.py ~30 lines — replace tk.Tk/mainloop with wx.App/MainLoop, wx.CallAfter for thread-safe dispatch, wx.CallLater for delayed calls
setup.py Add wxPython>=4.2.0 to install_requires
requirements.txt Add wxPython>=4.2.0
acb_sync/tray.py Docstring-only update (no functional change)

No changes to: config.py, notify.py, hotkeys.py, platform_utils.py, watcher.py, copier.py — none of these import tkinter.

Key wxPython patterns used

  • wx.ScrolledWindow replaces the Canvas+Scrollbar hack
  • wx.StaticBox + wx.StaticBoxSizer replaces ttk.LabelFrame
  • wx.SpinCtrl replaces ttk.Spinbox (returns int directly, no string parsing needed)
  • wx.Choice replaces ttk.Combobox(readonly)
  • wx.ListCtrl(LC_REPORT) replaces ttk.Treeview
  • wx.Timer replaces recursive root.after() for periodic refresh
  • wx.EVT_CHAR_HOOK replaces <KeyPress> binding for hotkey recording

Test plan

  • pip install -e . — verify wxPython installs and app launches
  • Tray icon appears, context menu works
  • Settings window: all sections render, scrolling works, Browse dialogs open, Save validates and persists
  • Status window: periodic refresh, copy history table, Pause/Resume button
  • HotkeyRecorder: Record → press keys → captured; Clear; Escape cancels
  • Escape closes both windows; resize adapts layout
  • macOS VoiceOver (Cmd+F5): tab through Settings controls, verify labels are announced
  • Windows NVDA/JAWS: verify controls are read correctly

🤖 Generated with Claude Code

Michael Babcock and others added 9 commits February 16, 2026 14:38
1. Remove [project] table from pyproject.toml
   The [project] table conflicted with setup.py metadata. Newer
   setuptools tried to merge both and crashed with an AttributeError
   because long_description, install_requires, and extras_require were
   defined in setup.py but not declared as dynamic in pyproject.toml.
   Removing [project] lets setup.py be the single source of truth;
   [tool.ruff] and [tool.pyright] sections are unaffected.

2. Gracefully handle macOS root requirement in hotkeys
   The keyboard library's listen() unconditionally checks
   os.geteuid() == 0 on macOS. Without root, it raises OSError in a
   background thread and triggers a SIGTRAP that kills the process.
   Now we mirror that same check in register() so hotkey registration
   is skipped gracefully, and install a threading.excepthook safety
   net. The app starts normally without hotkeys and logs a clear
   warning instead of crashing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tkinter's Tcl/Tk backend does not implement Apple's NSAccessibility
protocol, making the Settings and Status windows completely invisible
to VoiceOver on macOS.  wxPython uses native Cocoa widgets (and native
Win32 on Windows), so controls automatically participate in the OS
accessibility hierarchy — fixing VoiceOver and improving JAWS/NVDA.

- app.py: replace tk.Tk/mainloop with wx.App/MainLoop, wx.CallAfter,
  wx.CallLater; hidden wx.Frame anchors the event loop
- ui.py: full rewrite — ScrolledWindow, StaticBoxSizer, FlexGridSizer,
  ListCtrl, SpinCtrl, DirDialog, wx.Timer, EVT_CHAR_HOOK for hotkey
  capture; all controls get .SetName() for screen-reader labels
- requirements.txt / setup.py: add wxPython>=4.2.0 dependency
- tray.py: docstring-only update (no functional change)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- tray.py: sort imports (Any before Protocol) to satisfy ruff I001
- watcher.py: remove unused Dict/Tuple imports, sort remaining imports,
  wrap long watchdog import line
- ci.yml: install libgtk-3-dev before pip install so wxPython can build

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wxPython requires native GTK/Cocoa libs and takes 20+ minutes to
build from source on headless Linux CI runners.  Instead:
- Add minimal typestubs/wx/__init__.pyi (treats all wx attrs as Any)
- Point pyright at it via stubPath in pyproject.toml
- Skip wxPython in CI pip install (grep -v wxPython)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The grep -v | $() approach broke on lines with semicolons and == in
platform markers.  Write to a temp file and use pip install -r instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- pyproject.toml: use extraPaths (not stubPath) so pyright resolves
  import wx from typestubs/ even when wxPython is not installed
- ui.py: use local variables in _build() methods to avoid
  reportOptionalMemberAccess on self._win, self._list_ctrl, etc.
- watcher.py: cast event.src_path to str (watchdog types it bytes|str)
- notify.py: add type: ignore[import-not-found] for Windows-only
  accessible_output2
- platform_utils.py: add type: ignore[attr-defined] for winreg/winsound
  attributes unavailable on Linux

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename typestubs/wx/__init__.pyi to .py (extraPaths resolves .py
  not .pyi for module discovery)
- Add type: ignore[attr-defined] on each line referencing winreg
  constants (HKEY_CURRENT_USER, KEY_SET_VALUE, REG_SZ)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pyrightconfig.json overrides pyproject.toml settings. It was missing
the typestubs directory in extraPaths, so pyright couldn't resolve
import wx from the stub.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant