Skip to content

Associate/disassociate applications with app modules from NVDA settings (1.0.0)#2

Merged
marlon-sousa merged 10 commits into
mainfrom
feat/unassociate-and-current-app-prefill
Jul 15, 2026
Merged

Associate/disassociate applications with app modules from NVDA settings (1.0.0)#2
marlon-sousa merged 10 commits into
mainfrom
feat/unassociate-and-current-app-prefill

Conversation

@marlon-sousa

@marlon-sousa marlon-sousa commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

Reworks the add-on around a clear two-action model, addressing feedback from testing 0.3.0:

  • Associate an application with a module — a real one, or (not associated) to give it no app module (an action, done in one dialog).
  • Disassociate — delete a custom mapping and restore the application's original module.

Panel

  • Buttons: Associate app…, Disassociate.
  • Applications associated with no module show in the list as (not associated).
  • Convenience: when the category opens, if the app you were last in already has a custom mapping, its row is selected automatically — ready to disassociate or re-associate.

Associate dialog

  • App: editable combo pre-populated with the apps you currently have open (appModuleHandler.runningTable), the last-focused app preselected; you can pick another or type any executable by hand.
  • Module: (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. notAssociated never appears under its raw name.
  • OK enabled only when an app and a module are chosen and the pair isn't the association the app already has (no duplicate / no-op).

Safeguards

  • Case-insensitive app handling throughout (NVDA lowercases executables); panel model keyed by lowercased name.
  • Pickle protected from duplicates via dedupeMappings on both load and save (case-insensitive, later entry wins).
  • Versioned pickle format: stored as {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

  • Tracked via 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:

  • notAssociated hidden from the offered modules; real modules + alias targets present; nvda hidden.
  • getRunningApps: deduped, nvda excluded, sorted.
  • dedupeMappings: case-insensitive, later entry wins.
  • pickle format: persist writes the versioned dict; readMappingsFile reads both the versioned dict and a legacy bare list.
  • set/getLastForegroundApp round-trips.
  • flake8 passes (pre-commit); all modules byte-compile.

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

  1. Associate current app: Focus Notepad → NVDA Settings → Custom Application Module Mapper → Associate app…. App combo pre-filled with notepad and lists your open apps; module preselected to notepad's current module and OK disabled (no-op). Change the module → OK enables → confirm → row appears.
  2. (not associated) is a choice: In the module list, (not associated) is the first entry; there is no raw notAssociated.
  3. Associate with no module (detach) by hand: In Associate app…, type an app that isn't running, pick (not associated), OK → row app -> (not associated).
  4. Duplicate guard: Open Associate app… for an app already mapped to module X, pick X again → OK stays disabled.
  5. App with no module: Associate app… for an app that has only NVDA's base module → no module preselected, OK disabled until you pick one.
  6. Re-associate an existing mapping: Associate an app to module X and apply. Associate app… for the same app → its current module X is preselected and OK is disabled → pick a different module Y → OK enables → apply → the row now shows Y. Later Disassociate → restores the app's original module (not X or Y).
  7. Module preselect follows the app: Open Associate app… (prefilled with the current app), then change the app field to a different mapped app → the module combo updates to that app's current module (and OK disables as a duplicate until you change it).

Panel
8. Auto-select current app: Custom-associate Notepad and apply. Focus Notepad again → open the category → the notepad row 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 dbeaver goes back to eclipse; 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 → its appModule is 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 (appModule is the generic base AppModule, i.e. appModuleName is appModuleHandler).
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) then notepad → 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.pickle in 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

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>
@github-actions

Copy link
Copy Markdown

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>
@github-actions

Copy link
Copy Markdown

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>
@github-actions

Copy link
Copy Markdown

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

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>
@github-actions

Copy link
Copy Markdown

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>
@github-actions

Copy link
Copy Markdown

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>
@github-actions

Copy link
Copy Markdown

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>
@github-actions

Copy link
Copy Markdown

- 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>
@github-actions

Copy link
Copy Markdown

- 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>
@github-actions

Copy link
Copy Markdown

@marlon-sousa marlon-sousa changed the title Add unassociate action and current-app prefill Associate/disassociate applications with app modules from NVDA settings (1.0.0) Jul 14, 2026
@marlon-sousa marlon-sousa merged commit a512bb9 into main Jul 15, 2026
2 checks passed
@marlon-sousa marlon-sousa deleted the feat/unassociate-and-current-app-prefill branch July 15, 2026 01:23
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.

2 participants