Associate/disassociate applications with app modules from NVDA settings (1.0.0)#2
Merged
Merged
Conversation
Detach an application from any app module directly from the settings panel, and pre-detect the application the user was in when opening NVDA Settings so they no longer need to type executable names by hand. - Track the last real (non-NVDA) foreground application via event_foreground in the global plugin, skipping NVDA's own windows so the stored value keeps pointing at the app the user came from. - Hide the notAssociated sentinel from the module list; it is now reached only through the dedicated unassociate action. - Add an "Unassociate current app" button that detaches the current app, and show detached apps in the list as "(not associated)". - Pre-fill the Add mapping dialog with the current app and its current module. - Update README to document detaching and current-app detection, and fix long-standing typos. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged add-on artifact |
Treat unassociation as an association to the notAssociated module, and make both reachable from one dialog while adding safeguards. - Associate dialog: the app field is now an editable combo pre-populated with the currently open applications (from appModuleHandler.runningTable), the last-focused app preselected; the module list offers "(not associated)" first, so detaching is just another choice and can be typed by hand. - OK is enabled only when both an app and a module are chosen and the pair is not the association the app already has (no duplicate/no-op). - Unassociate current app now confirms, naming the app, before staging. - Application names are handled case-insensitively (NVDA lowercases executables); the panel model is keyed by lowercased name. - Protect the persisted pickle from duplicates via dedupeMappings on both load and save (case-insensitive, later entry wins). - Track the last focused app via event_gainFocus instead of event_foreground so it is populated at startup and stays fresh; the unassociate button is disabled until an app has been seen. - README updated to describe the new flow, case-insensitivity and dedup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged add-on artifact |
The unassociate action was hard-wired to the foreground app, so an already-associated app selected in the list could not be detached. - Unassociate now targets the selected mapping when one is selected, falling back to the last focused app otherwise; the button label updates to name what it will act on. - Enable matrix: a selected mapping is detachable unless already detached; the current-app fallback is enabled for a custom or an internally associated app, but disabled when the app has no app module at all (nothing to unassociate) or is already detached. - Tracker records whether the focused app had a specific module (type(appModule) is not the base AppModule) to drive that decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged add-on artifact |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged add-on artifact |
Per the clarified model, marking an app to no module is an association action (done in the Associate dialog by choosing "(not associated)"), not a disassociation. "Disassociating" means removing a custom mapping to restore the original, which is what Remove mapping already does. - Remove the Unassociate button and all its selection-aware machinery; detaching now goes through Associate app… (which already pre-fills the current app), and reverting goes through Remove mapping. - Convenience: when the panel opens, if the app the user was last in has a custom mapping, its row is selected by default. - Simplify the foreground tracker back to (app, module); the hasSpecificModule flag and appModuleHandler import are no longer used. - README updated to the two-action model and the auto-selection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged add-on artifact |
Matches the terminology used throughout: removing a custom mapping is disassociating the application (restoring its original module). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged add-on artifact |
The button was always enabled, so it looked actionable even with nothing selected (onRemove silently no-oped). Track the list selection and enable Disassociate only when a mapping is selected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged add-on artifact |
Two fixes from testing: - getRunningApps now enumerates the processes owning a visible, titled, top-level window (merged with appModuleHandler.runningTable), instead of relying on runningTable alone. runningTable only holds apps NVDA already built a module for this session, so open apps like VS Code and cmd were missing from the picker. - After Disassociate removes the selected (or only) mapping, keep focus on the list (selecting a neighbouring row when one remains) instead of letting it jump to the dialog's OK button when the Disassociate button becomes disabled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged add-on artifact |
- Associate dialog: when the application field changes, re-preselect the module that application is currently associated with (or clear it for an unknown app), so switching apps mid-dialog no longer leaves the previous app's module selected. Re-associating an already-associated app to a different module was already supported (MODIFY path); this makes the starting state and the duplicate guard correct per app. - README: add a "What is an app module association?" section and a "Why would you use this add-on?" section, document re-association and the guarantee that disassociation always restores the true original, and give a complete feature list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged add-on artifact |
- Store the mappings pickle as {formatVersion, mappings} instead of a
bare list, so a future format change can be detected and migrated.
readMappingsFile understands both the new versioned dict and the
pre-1.0 bare list (format version 0), staying backward compatible.
- Bump addon_version to 1.0.0 and rewrite the changelog for a first
complete release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged add-on artifact |
thgcode
approved these changes
Jul 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reworks the add-on around a clear two-action model, addressing feedback from testing 0.3.0:
(not associated)to give it no app module (an action, done in one dialog).Panel
(not associated).Associate dialog
appModuleHandler.runningTable), the last-focused app preselected; you can pick another or type any executable by hand.(not associated)first, then every available module. Selecting it associates the app with no module; selecting a real module maps it. This single dialog associates any app — running or not — with a module or with nothing.notAssociatednever appears under its raw name.Safeguards
dedupeMappingson both load and save (case-insensitive, later entry wins).{formatVersion, mappings}so a future format change can be detected/migrated; reads both the new dict and the pre-1.0 bare list.Current-app detection
event_gainFocus(skipping NVDA's own windows) so it's populated at startup and stays fresh, and drives both the dialog pre-fill and the auto-selection.Version bumped to 1.0.0 (first complete release; 0.3.0 was never shipped). README rewritten with a "What is an app module association?" concept section, a "Why use this add-on?" section, the two-action model, re-association, case-insensitivity and dedup, plus long-standing typo fixes.
Automated verification
Backend logic tested with stubbed NVDA deps:
notAssociatedhidden from the offered modules; real modules + alias targets present;nvdahidden.getRunningApps: deduped,nvdaexcluded, sorted.dedupeMappings: case-insensitive, later entry wins.persistwrites the versioned dict;readMappingsFilereads both the versioned dict and a legacy bare list.set/getLastForegroundAppround-trips.The GUI (combo, dynamic OK, auto-selection) can't be driven headless — covered by the manual plan below.
Functional test plan (in NVDA)
Associate dialog
notepadand lists your open apps; module preselected to notepad's current module and OK disabled (no-op). Change the module → OK enables → confirm → row appears.(not associated)is a choice: In the module list,(not associated)is the first entry; there is no rawnotAssociated.(not associated), OK → rowapp -> (not associated).Panel
8. Auto-select current app: Custom-associate Notepad and apply. Focus Notepad again → open the category → the
notepadrow is selected automatically. Focus an app with no mapping → open the category → no row is force-selected.9. Disassociate disabled with no selection: With no row selected, Disassociate is disabled; selecting a row enables it.
10. Disassociate reverts: Select a custom row → Disassociate → confirm the dialog → the app returns to its original module (e.g. a customized
dbeavergoes back toeclipse; a same-name app back to its built-in module). No need to remember the original.11. Disassociate a detached row: Select a
(not associated)row → Disassociate → the app returns to its original module.12. Focus stays in the list: After disassociating the selected (or only) row, focus stays on the list (a neighbouring row is selected when one remains), not the dialog's OK button.
Runtime effect (the actual purpose — verify with NVDA+F1 Developer Info or the Python console:
import api; api.getFocusObject().appModule.appModuleName)13. Association applies live: Associate an application with a real module (e.g. a Notepad-like app →
code) and confirm. Without restarting NVDA, focus that app → itsappModuleis now the mapped module (app modules are reloaded on save, so it applies immediately).14. Detach applies live: Associate an application that has a built-in same-name module (or an internal alias like
dbeaver) with(not associated), confirm → focus it → NVDA applies no app specific behavior (appModuleis the generic baseAppModule, i.e.appModuleNameisappModuleHandler).15. Re-association applies live: Re-associate an already-mapped app to a different module, confirm → focus it → it now uses the new module.
Cross-cutting
16. Open-apps picker: With several apps open (e.g. VS Code, cmd, Explorer), the Associate dialog's app combo lists them — not just the couple NVDA already had modules for.
17. Case-insensitive: Associate
Notepad(mixed case) thennotepad→ only one row/entry; restart NVDA → no duplicate persists.18. Persistence: Associate an app with
(not associated), confirm, restart NVDA → still(not associated).19. Survives reinstall/update: Create associations, apply, then reinstall or update the add-on → associations are still present (they live in the NVDA config dir, not the add-on folder).
20. Legacy migration: With a pre-1.0
customModulesMapping.picklein the add-on folder and none in the config dir, on next start it is migrated to the config dir and applied. A bare-list (format 0) file is read and, once re-saved, stored as the versioned{formatVersion, mappings}format.🤖 Generated with Claude Code