Skip to content

Add fully configurable keyboard shortcuts throughout Gramps - #2463

Open
dsblank wants to merge 7 commits into
gramps-project:masterfrom
dsblank:feature/custom-keybindings
Open

Add fully configurable keyboard shortcuts throughout Gramps#2463
dsblank wants to merge 7 commits into
gramps-project:masterfrom
dsblank:feature/custom-keybindings

Conversation

@dsblank

@dsblank dsblank commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

Makes keyboard shortcuts fully user-configurable across Gramps, via a new
Keyboard Shortcuts tab in Preferences. Previously, shortcuts were
scattered across several different, non-overlapping mechanisms (Gio.SimpleAction
accelerators for the main menu/toolbar/views, static <accelerator> tags in
editor-dialog .glade files, and even a hardcoded key-press-event handler
for Ctrl+C) with no way for a user to see or change any of them. This PR
unifies all of them under one system.

Screenshot from 2026-07-13 08-09-07

What you can do now

  • Preferences → Keyboard Shortcuts: a searchable table of every shortcut
    in the app — main menu/toolbar, every List/Navigation view (People, Events,
    Places, …), the Note editor's rich-text toolbar, Tools/Reports menu
    entries, and every "Add"/"Private"/"Date editor" style button inside the
    Person/Event/Family/etc. editor dialogs.
  • Click a shortcut cell, press a new key combo (Gtk.CellRendererAccel),
    done — applies live for anything currently open, with a conflict warning
    if the combo is already in use elsewhere.
  • Reset to Factory Default (single row) / Reset All to Factory
    Defaults
    (everything) — always correct for your platform, since it
    reads live defaults rather than a snapshot, regardless of what your
    active theme currently has saved.
  • Themes: there's always one active theme (shown as "Theme:" in the
    editor), and every rebind/reset is saved straight into it — no separate
    unsaved state to track. Picking a different entry in the combo switches
    to it immediately. New Theme… forks your current bindings under a
    new name and makes that the active theme. A bundled Default theme
    ships as an editable starting point; editing it forks a personal copy
    automatically the first time you save.
  • Alt+1..Alt+9 inside any Person/Family/Event/etc. editor dialog jump
    straight to that numbered tab — no more tabbing through every widget
    just to reach, say, the Events list before adding a new one.

In plain terms (why 189 shortcuts, and what's not on the list)

For anyone reading this who isn't a developer:

Almost every command in Gramps now has a keyboard shortcut you can change,
found in Preferences → Keyboard Shortcuts. That includes the main menu and
toolbar, every view's own commands (People, Events, Places, etc.), the Note
editor's formatting buttons, and the small icon buttons inside editing
windows (like the "Private" lock icon, or the button that opens the date
picker). Right now that adds up to 189 shortcuts you can search, rebind, or
reset — with Gramps warning you if a new choice is already used somewhere
else.

What's not on that list: the underlined letters you see when you hold Alt
in an editing window (like the underlined "T" in "Type" or "N" in
"Nickname"). Those aren't separate commands — they're just a quick way to
jump straight to a specific box on the screen, built directly into that
box's label. Because they're tied to the label's own wording (and that
wording is translated differently in every language), there's no separate
"shortcut" underneath them to reassign. They'll keep working exactly as
they always have; they just won't show up in the Keyboard Shortcuts list.

How editor-dialog shortcuts become overridable

Since GtkBuilder wires <accelerator> tags up entirely on its own with zero
Python hooks, there's no Gio.SimpleAction to rebind. Converting ~24 editor
dialogs to the action-based system would have been a much larger, riskier
change (each dialog would need to become "application-aware" early, and
button wiring uses two different existing patterns). Instead,
Glade.__init__ now rewrites the relevant <accelerator> XML attributes at
load time based on a saved override, right alongside the existing macOS
Control→Meta remap it already did. Verified by round-tripping every
accelerator-bearing .glade file (with real widget types registered)
through a real Gtk.Builder under Xvfb — all 26 load correctly.

One consequence: conflict detection for these is scoped to the same dialog
only (different dialogs are never open at once, so the same key reused
across two unrelated dialogs isn't a real conflict) — confirmed empirically
during development, where global conflict-checking was flagging the same
key as "in use" across five unrelated dialogs.

Also fixed: Ctrl+C

Ctrl+C in the main window silently opened the Clipboard window — not
because it was bound to that action (Ctrl+B is), but because
NavigationView.key_press_handler hardcoded a raw Gdk.KEY_c check that
called copy_to_clipboard() directly, entirely outside the action/accelerator
system. It's now a normal win.CopyToClipboard action with the same default
binding, so it's visible and rebindable like everything else.

Notable findings during development

  • Two duplicate-accelerator bugs in Gramps' own .glade files, now
    surfaced as warnings at startup instead of being silently invisible:
    editpersonref.glade has two different, simultaneously-visible buttons
    (select, add_del) both hardcoded to Ctrl+S; importprogen.glade has
    two Private toggles both hardcoded to Ctrl+P. Not fixed in this PR.
  • Gtk.accelerator_parse/accelerator_name silently drop virtual modifiers
    like <Primary> without a real display connection, which would have
    quietly corrupted accelerator strings under this project's headless
    (GDK_BACKEND=-) test convention. _normalize_accel() detects that case
    and passes the value through unchanged instead.

Out of scope

  • Native GTK text-widget bindings (Ctrl+C/V/X/A/Z inside a GtkEntry) —
    structurally unreachable from the action system.
  • Dynamically generated menus with no accelerator today (Recent Files,
    Tag menu, bookmark-jump submenu, gramplet popups) and MRU
    navigation-history jump slots — session/content-dependent, not fixed
    named commands.
  • The two duplicate-accelerator bugs found above.
  • Field-navigation mnemonics (the Alt+underlined-letter that jumps to a
    field, e.g. Alt+T for "_Type:") — see "In plain terms" above for why.

Update (2026-07-13)

  • Added global, customizable Accept Dialog (OK) / Cancel Dialog
    shortcuts (default Alt+O / Alt+C, registered via
    register_static_shortcuts) that work in any editor dialog. Plain
    Gtk.Dialog windows don't get GtkApplication's automatic
    accelerator-to-action routing even once associated with the app via
    set_application(), so ManagedWindow now wires its own
    Gtk.AccelGroup and, on activation, clicks the dialog's actual
    OK/Cancel button (looked up via get_widget_for_response) — most
    editors wire their save/cancel logic to the button's own clicked
    signal rather than the dialog's response signal, so emitting
    response() directly would have been a no-op for them.
  • Removed the hardcoded _OK/_Cancel mnemonics from editor .glade
    files now that these are driven by the shortcut above instead.
  • Fixed glade tooltip-label lookup: it was checking the property name
    tooltip_text, but GTK's actual glade property is tooltip-text, so
    tooltip-derived shortcut labels were silently never found. Also now
    takes only the first line of a tooltip, since a few tooltips continue
    with further explanation on subsequent lines.
  • Fixed a "WindowManger" typo in the internal action-group name
    (gramps/gui/managedwindow.py).
  • Excluded the WindowManager action group from the shortcut
    editor.
    Its actions are generated one-per-open-window for the
    "Windows" switcher list (wm-<window_id>, where window_id is often
    a Python id() of a class/instance) — not a fixed command, so the
    action only exists while that specific window happens to be open, and
    its id isn't stable across app restarts. It was showing up in the
    editor as a confusing, ever-changing "Preferences..." row (etc.)
    alongside the real, static Application → Preferences... shortcut.
    A new _DYNAMIC_GROUP_NAMES set in uimanager.py now excludes it
    (and is the place to add any future per-instance group).
  • Reworked keyboard shortcut themes into a persistent, named model,
    matching how VS Code/JetBrains handle keymap profiles, instead of the
    previous one-shot Load/Save import-export:
    • A new interface.keybinding-theme config key tracks which theme is
      active; every rebind, clear, or reset now saves directly into that
      theme's own file (gramps/gui/uimanager.py's new theme_dirs() /
      theme_path() helpers, shared between the editor and app startup)
      instead of a separate, anonymous gramps.accel override file.
    • Editing the bundled Default theme forks a personal copy into the
      user's keybinding_themes directory the first time it's saved, since
      user themes already take precedence over same-named bundled ones.
    • The "Theme:" combo now switches themes immediately on selection — the
      separate "Load"/"Switch Theme" button is gone, since every edit is
      already persisted and there's never unsaved state to protect.
      "Save…" is replaced by New Theme…, which forks the current
      bindings under a new name and makes it active.
    • Reset to Default / Reset All to Defaults are renamed to
      Reset to Factory Default / Reset All to Factory Defaults to
      make explicit that they always read Gramps' live, hardcoded
      accelerator defaults, not a theme's own saved values.
    • Renamed the bundled Default-PC theme to Default: GTK already
      renders <Primary> as the platform's own modifier (e.g. Cmd on
      macOS), so the single bundled file was never actually PC-specific,
      and the "-PC" suffix wrongly implied a separate Mac variant that
      doesn't exist.

Update (2026-07-13, accessibility + theme file format)

Follow-up review raised an accessibility concern with the OK/Cancel change
above: removing _OK/_Cancel also removed the GTK-native accelerator
binding on those buttons, which is what exposes a keybinding to assistive
technology through ATK — so a screen reader lost any indication that
Alt+O/Alt+C activate those buttons, not just the visual Alt-hold underline
for sighted keyboard users.

  • The _OK/_Cancel mnemonics are restored in all ~42 editor-dialog glade
    files. ManagedWindow now only intervenes when a user has actually
    customized "Accept Dialog (OK)"/"Cancel Dialog" away from its default: it
    strips the (now stale) mnemonic from that button's label, binds the real
    key via Gtk.Widget.add_accelerator() — a genuine widget-level GTK
    accelerator, so it still shows up in the button's ATK keybinding, unlike
    the previous bare Gtk.AccelGroup callback — and shows the current
    shortcut as a tooltip, since an arbitrary user-chosen key (e.g. F9)
    can't be represented as an underlined letter in a translated label at
    all. Left at its default, the glade-defined mnemonic does all the work
    untouched, exactly as before this whole feature existed.

Also switched the keyboard shortcut theme file format from a single
Python-dict-literal blob (*.accel) to JSON Lines (*.jsonl), one
{"id", "label", "category", "accel"} object per line:

  • Each entry carries its own human-readable label and category, so a theme
    file is self-documenting when opened directly in a text editor, instead
    of a bare {action_id: accel} mapping that requires cross-referencing
    the shortcuts editor to make sense of — these files live in a normal,
    user-writable config directory, so hand-editing them was always possible
    even though nothing documented it.
  • A malformed line (or one with a now-disallowed accelerator) is logged and
    skipped rather than aborting the whole file, so a single typo in a
    hand-edited theme no longer costs every other binding in it.
  • Startup now tolerates a theme file that can't be loaded at all (missing,
    unreadable, or otherwise malformed) instead of crashing before the main
    window ever appears, which a hand-edited file could previously trigger
    via an uncaught SyntaxError in GrampsApplication.do_startup().
  • The bundled "Default" theme is converted to the new format, with labels
    and categories initially filled in wherever mechanically derivable from
    glade files and static action registrations (69 of 104 entries) —
    superseded below by a script that generates it from a real, running
    session instead.

Validating theme entries on load this way surfaced two pre-existing
check_accel() bugs, both now fixed: it rejected Left/Right/Up/Down
whenever used with any modifier, which would have flagged Gramps' own
default "Go Back"/"Go Forward" bindings (<Alt>Left/<Alt>Right) as
invalid the moment the bundled theme was loaded; and it could not resolve
the virtual <Primary> modifier without a live display connection, causing
false rejections under this project's headless test convention
(GDK_BACKEND=-).

Update (2026-07-13, regeneration script)

The mechanical, display-less conversion of the bundled "Default" theme
mentioned above could only resolve labels for actions known statically at
startup (69 of 104 entries) — most ordinary per-view menu/toolbar actions
(e.g. win.NewTag, win.SourceAdd) are only known once that view's real
menu XML has actually been built against a live display, which a headless
conversion can't do.

Added scripts/regenerate_default_theme.py to close that gap: it boots
the real UIManager/ViewManager stack under a real X display (not the
full GrampsApplication, since that would auto-open the Family Tree
manager dialog) against an isolated, throwaway GRAMPSHOME — so it never
reads or writes a developer's real settings, and never loads an existing
accel/theme file, only Gramps' true hard-coded defaults — visits every
registered view once so each view's own actions register, then calls
save_accels(only_changed=False) and copies the result over
gramps/gui/keybinding_themes/Default.jsonl.

xvfb-run -a python3 scripts/regenerate_default_theme.py

Re-run it after adding, renaming, or removing a keyboard shortcut anywhere
in Gramps, then review the diff before committing. Running it against the
current tree raised coverage from 104 entries (69 labeled) to 189
entries (187 labeled)
— the 2 that still come back unlabeled reflect a
real, pre-existing gap in Gramps' own menu labeling for those two actions
(win.F2, win.PrintView), not a script limitation. save_accels()'s
only_changed=False branch writes entries sorted() by id, so re-running
the script is deterministic — confirmed by diffing two consecutive runs —
and a future re-run's diff will only ever show genuinely added, removed,
or changed shortcuts, never spurious reordering.

Update (2026-07-15, direct tab-jump shortcuts)

A reviewer pointed out that reaching a given tab in a primary-object editor
(Person, Family, Event, …) with only the keyboard meant tabbing through
every preceding widget — e.g. 12 Tab presses just to reach the Events
list before Insert could add anything. That's not specific to Events:
the Person editor alone has 10 addable tabs (Events, Names, Source
Citations, Attributes, Addresses, Notes, Gallery, Internet, Associations,
LDS), all ButtonTab-derived and already supporting Insert/Delete/
Enter. A one-off "new event" shortcut wouldn't generalize; what was
missing was a fast way to reach a tab in the first place.

Added Alt+1..Alt+9 to jump directly to notebook tab N, registered as
static shortcuts (app.dialog-goto-tab-1..9) right alongside
dialog-ok/dialog-cancel, so they show up in Preferences → Keyboard
Shortcuts and are fully rebindable/themeable. They're wired once in
EditPrimary._setup_notebook_tabs (shared by every primary-object editor),
using the same per-dialog Gtk.AccelGroup pattern as the existing
dialog-ok/dialog-cancel wiring, since a bare Gtk.Dialog gets no
automatic accel routing from GtkApplication. Editors with fewer than 9
tabs simply get fewer bound keys; the existing Alt+Left/Alt+Right
sequential stepping is untouched. Net effect: "open Person, add an Event"
goes from ~13 keystrokes to 2 (Alt+1, Insert).

Testing

  • gramps/gui/test/uimanager_test.py (53 tests) — accel default tracking,
    label resolution, set/clear/reset, save/load round-trip in the new JSONL
    format (including malformed-line, disallowed-accel, and unreadable-file
    handling), static registration for never-instantiated actions, glade-scope
    conflict detection, the modified-nav-key check_accel() fix, and
    exclusion of the WindowManager group from list_actions().
  • gramps/gui/test/glade_test.py (15 tests) — XML scanning, modifier
    conversion (pure string ops, no display dependency), override
    application, multiline-tooltip handling, and a smoke test parsing
    every .glade file under GLADE_DIR.
  • gramps/gui/test/managedwindow_test.py (4 tests, new) — the OK/Cancel
    accelerator wiring: default binding leaves the mnemonic/tooltip untouched,
    a customized binding strips the mnemonic and sets a tooltip, a cleared
    binding strips the mnemonic with no tooltip, and non-dialog windows are a
    no-op.
  • mypy and black --check clean across the full tree.
  • Manual: ran scripts/regenerate_default_theme.py twice in a row under
    xvfb-run, confirmed the two runs produce byte-identical output, and
    confirmed the resulting Default.jsonl loads with zero rejected entries.
  • Manual: every accelerator-bearing .glade file loaded through a real
    Gtk.Builder under Xvfb after rewriting, confirming the override
    mechanism doesn't corrupt dialog layout.
  • Manual, full app under Xvfb (real GrampsApplication + ViewManager,
    isolated GRAMPSHOME): opened Preferences → Keyboard Shortcuts and
    screenshotted it; rebound a shortcut and confirmed it's written into
    the active theme's file; used New Theme to fork a second theme and
    confirmed the combo/config switch to it; switched back via the combo
    and confirmed the (customized) bindings reloaded; used Reset to
    Factory Default and confirmed it restores the true hardcoded default
    rather than the theme's saved value, and that the reset itself is
    then persisted back into the active theme's file.
  • Manual, full app under Xvfb: opened a Person editor (11 tabs) and
    confirmed Alt+2/Alt+1/Alt+9 jump to the corresponding tab; landed
    on Events via Alt+1 and confirmed Insert opens the Event Reference
    Editor with no mouse involved; opened the Note editor (2 tabs) and
    confirmed Alt+1/Alt+2 work while Alt+3..Alt+9 are silent no-ops;
    rebound Go to Tab 1 to Ctrl+1 in Preferences and confirmed the new
    binding takes effect immediately and the old Alt+1 no longer does.

🤖 Generated with Claude Code

Users can now view, search, rebind, reset, and save keyboard shortcuts
for essentially every command in Gramps through a new Keyboard
Shortcuts tab in Preferences: the main menu and toolbar, every
List/Navigation view, the Note editor's formatting toolbar, Tools and
Reports menu entries, and every "Add"/"Private"/"Date editor" style
button inside the Person, Event, Family, and other object editor
dialogs. Every shortcut, including ones belonging to a view or dialog
never opened this session, appears in the list and is editable, since
each source (main window, per-view, per-dialog) registers its actions
statically rather than only when instantiated.

Rebinding a shortcut applies immediately for anything currently open,
warns about and offers to resolve conflicts with other actions bound
to the same key, and persists to a per-user file. A keyboard shortcut
"theme" system lets users save their current bindings under a name and
load a saved set later; a bundled "Default-PC" theme captures Gramps'
own shipped non-mac defaults as an editable starting point, while
"Reset All to Defaults" remains the authoritative, always
platform-correct way back to Gramps' actual shipped bindings.

Also fixes a long-standing surprise where Ctrl+C in the main window
silently opened the Clipboard window instead of behaving like a normal
shortcut: that behavior was previously a hardcoded key-press handler
living outside the action system entirely, invisible to any
customization. It is now a regular, rebindable "Copy to Clipboard"
action like everything else.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dsblank dsblank added this to the v6.2 milestone Jul 11, 2026
dsblank and others added 2 commits July 13, 2026 07:21
Adds global, customizable "Accept Dialog (OK)" / "Cancel Dialog"
shortcuts that are wired to a dialog's real OK/Cancel button even
though plain Gtk.Dialog windows don't get GtkApplication's automatic
accelerator routing. Glade tooltip lookup now uses the correct
tooltip-text property name (was tooltip_text) and takes only the
first line, since some tooltips continue with further explanation.
Hardcoded "_OK"/"_Cancel" button mnemonics are removed from the glade
files now that these actions are driven by the shortcut system. Fixes
a "WindowManger" typo in the WindowManager action group name, and
excludes that group from the shortcut editor entirely, since its
actions are generated per open-window instance and are not stable,
customizable commands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The shortcut editor's Load/Save buttons treated a "theme" as a
one-shot import/export: loading flattened it into an anonymous
per-user override file, and nothing remembered which theme, if any,
was active. Preferences now tracks the active theme by name via a new
interface.keybinding-theme config key, and every edit -- rebind,
clear, or reset -- is saved directly into that theme's own file
instead of the old gramps.accel override.

Editing the bundled "Default" theme forks a copy into the user's
keybinding_themes directory the first time it's saved, since user
themes already take precedence over same-named bundled ones.

Selecting a different theme in the "Theme:" combo now switches to it
immediately, so the separate "Load"/"Switch Theme" button is gone --
every edit is already persisted, so there is never unsaved state to
protect. "Save..." is replaced by "New Theme...", which forks the
current bindings under a new name and makes it active.

"Reset to Default" and "Reset All to Defaults" always read Gramps'
live, hardcoded accelerator defaults rather than a theme's own saved
values -- renamed to "Reset to Factory Default" / "Reset All to
Factory Defaults" to make that distinction clear.

Renamed the bundled "Default-PC" theme to "Default": GTK already
renders <Primary> as the platform's own modifier (e.g. Cmd on macOS),
so the single bundled file isn't PC-specific, and the "-PC" suffix
wrongly implied a separate Mac variant that doesn't exist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dsblank
dsblank marked this pull request as ready for review July 13, 2026 15:18
@dsblank

dsblank commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Does switching from Gtk3 to Gtk4 have any impact on these changes?

  • Most of the work — uimanager.py's action routing (app.set_accels_for_action, Gio.SimpleAction), the keybinding themes model, the editor/dialog — is built on Gio.Action/Gtk.Application accelerator plumbing. That's already the GTK4-style pattern (GTK4 kept set_accels_for_action), so it should port forward with little friction.
  • The pain point is managedwindow.py:536-571: it creates a raw Gtk.AccelGroup, calls window.add_accel_group(), and uses Gtk.accelerator_parse + Gtk.AccelFlags.VISIBLE to wire the dialog-ok/dialog-cancel shortcuts onto plain Gtk.Dialogs (since GtkApplicationWindow's auto-routing doesn't reach dialogs in GTK3). Gtk.AccelGroup and add_accel_group don't exist in GTK4 at all — it's replaced by Gtk.ShortcutController/Gtk.Shortcut. That whole block would need a rewrite, not a tweak, on a GTK4 port.
  • Gtk.accelerator_parse itself still exists in GTK4 (signature is basically the same), so uimanager.py:90/124 and configure.py:891 are fine as-is.

@dsblank

dsblank commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Are there any backwards incompatibility issues with this PR? I note that pressing ALT in a dialog no longer highlights the field or button (minor but a difference). Anything else?

Backwards-incompatibility issues in feature/custom-keybindings

  1. Alt-mnemonic highlighting on OK/Cancel — confirmed, real regression, by design

Across ~42 glade dialogs (editperson.glade, editevent.glade, editname.glade, dialog.glade, etc.), the OK/Cancel button labels were changed from _OK/_Cancel to plain OK/Cancel, stripping the GTK mnemonic markup. In its place, managedwindow.py:519-570 manually wires a global app.dialog-ok / app.dialog-cancel accelerator (default o / c, user-configurable) onto a Gtk.AccelGroup added to each dialog window, and calls .clicked() on whatever widget owns that response id.

Net effect for the default theme: Alt+O and Alt+C still work exactly as before. But since GTK's "hold Alt → underline the mnemonic letter" behavior is driven by the mnemonic markup in the label, and that markup is gone, holding Alt no longer visually underlines the OK/Cancel button (your observation). In dialogs where OK/Cancel were the only mnemonic widgets (e.g. editurl.glade, editname.glade), holding Alt now visually does nothing at all, which reads as "mnemonics are broken" even though other dialogs (editperson, editevent, which still have _Given name:-style field mnemonics) will still show underlines elsewhere.

This is a deliberate tradeoff, not an oversight — it's what makes OK/Cancel globally reassignable via the new shortcuts editor instead of being fixed per-dialog GTK mnemonics — but it's an unannounced visual/UX regression and undocumented in the code or tests.

  1. Translation churn on 42 dialogs

Changing _OK/_Cancel → OK/Cancel changes the translatable msgid in every one of those glade files. Existing translations for _OK/_Cancel won't match the new strings, so those buttons will silently fall back to untranslated English in every locale until translators catch up — a temporary but real regression for non-English users right after merge.

  1. WindowManger → WindowManager typo fix (managedwindow.py)

This renames a pre-existing (already-shipped) ActionGroup name. The only path by which this could actually break something for an existing user is the diagnostic-only dump_all_accels() → load_accels() round trip (never exposed via UI, dev-only). Low risk, but worth a mention in the commit message since it's a rename of a shipped identifier, not new code.

  1. NavigationView.call_copy() renamed to cb_copy_to_clipboard()

Consistent with your cb_ callback-naming convention, but it's a rename of a method that (in principle) an out-of-tree view subclass or addon could have overridden to customize Ctrl+C behavior. I found no such override in-tree or in the parts of addons-source I can see from here, so this is low-risk but technically a public-ish API break.

  1. Maintenance footgun: keybinding_themes/Default.accel can drift from hard-coded defaults

Default.accel is a static, checked-in snapshot of the current defaults, and it is loaded automatically for every user via theme_path("Default") in grampsgui.py (not just used as a reference). If a future commit changes a hard-coded default accelerator in Python without regenerating this file, the stale entry in Default.accel will silently override the new default for all users, since it's merged in over default_accels at startup. Not a problem today, but a landmine for later PRs unless there's a lint/test step keeping the two in sync (I didn't find one).

Nothing here touches the database schema or on-disk family tree data — this is all GUI-layer, so no upgrade.py/schema migration concerns.

dsblank and others added 3 commits July 13, 2026 12:38
Restores the "_OK"/"_Cancel" mnemonics stripped from ~42 editor-dialog
glade files by the previous commit. Removing them had also removed
the GTK-native accelerator binding on those buttons, which is what
exposes a keybinding to assistive technology via ATK -- so screen
readers lost any indication that Alt+O/Alt+C activate those buttons,
not just the visual Alt-hold underline.

ManagedWindow now only intervenes when a user has actually customized
"Accept Dialog (OK)"/"Cancel Dialog" away from its default: it strips
the (now stale) mnemonic from that button's label, binds the real key
via Gtk.Widget.add_accelerator() -- a genuine widget-level GTK
accelerator, so it still shows up in the button's ATK keybinding --
and shows the current shortcut as a tooltip, since an arbitrary
user-chosen key cannot be represented as an underlined letter in a
translated label. When left at its default, the glade-defined
mnemonic is untouched and does all the work, same as before this
whole keyboard-shortcuts feature existed.

Also switches the keyboard shortcut theme file format from a single
Python-dict-literal blob (`*.accel`) to JSON Lines (`*.jsonl`), one
`{"id", "label", "category", "accel"}` object per line:

- Each entry now carries its own human-readable label and category,
  so a theme file is self-documenting when opened directly in a text
  editor, rather than a bare {action_id: accel} mapping that requires
  cross-referencing the shortcuts editor to make sense of.
- A malformed line (or one with a disallowed accelerator) is logged
  and skipped rather than aborting the whole file, so a single typo in
  a hand-edited theme no longer costs every other binding in it.
- Startup now tolerates a theme file that can't be loaded at all
  (missing, unreadable, or otherwise malformed) instead of crashing
  before the main window ever appears, which a hand-edited file could
  previously trigger via an uncaught SyntaxError.
- The bundled "Default" theme is converted to the new format, with
  labels/categories filled in wherever mechanically derivable from
  glade files and static action registrations.

Fixes two check_accel() bugs surfaced by validating theme entries on
load: it rejected Left/Right/Up/Down whenever used with any modifier,
which would have flagged Gramps' own default "Go Back"/"Go Forward"
bindings (<Alt>Left/<Alt>Right) as invalid; and it could not resolve
the virtual <Primary> modifier without a live display connection,
causing false rejections under this project's headless test
convention (GDK_BACKEND=-).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Boots the real UIManager/ViewManager stack under a real X display
(not the full GrampsApplication, which would auto-open the Family
Tree manager dialog) against an isolated, throwaway profile, visits
every view once so each view's own menu/toolbar actions register,
then writes every known shortcut with its label and category to
gramps/gui/keybinding_themes/Default.jsonl. This is the only way to
get accurate labels for actions that aren't part of a view's static
get_shortcut_specs(), since those are only known once the real menu
XML is built against a live display.

Re-running it against the current codebase raises coverage from 104
entries (69 labeled, from a mechanical, display-less conversion) to
189 entries (187 labeled), since visiting every view surfaces each
view's own ordinary menu actions that the static registration alone
does not.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Switching views tears down the previous view's action groups and
menu XML (ViewManager.__disconnect_previous_page), so
UIManager.list_actions() -- and therefore the shortcut editor and
save_accels() -- only ever reported a label/category for whichever
view happens to be on screen right now. Any other view's own menu
action (e.g. a Geography view's "Print..." command) would show blank
the rest of the time, and permanently blank in a one-shot dump like
scripts/regenerate_default_theme.py, which showed up there as
win.F2/win.PrintView losing their label the moment a later view
became active.

UIManager now keeps a small label_cache, populated the same way
default_accels already is: the first time an action's label/category
is resolved while its group is live, list_actions() remembers it, and
falls back to that cached value (or the same id-derived text
get_action_label() already falls back to for a live-but-unlabeled
action) for any action currently known but not live. Also excludes
the WindowManager group from the new fallback path, matching the
existing exclusion for currently-live groups, since its per-open-
window ids were otherwise leaking back in.

scripts/regenerate_default_theme.py now calls list_actions() once per
visited view, seeding the cache while each view is still live, rather
than only once at the very end. Regenerated
gramps/gui/keybinding_themes/Default.jsonl: all 189 entries are now
labeled and categorized (previously 2 were blank), and win.PrintView
recovered its real menu label "Print..." instead of the id-derived
fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@emyoulation

Copy link
Copy Markdown
Contributor

Hopefully in the right thread this time...,

Since a "Help -> Keybindings" menu item already exists (which links to the Wiki page: https://gramps-project.org/wiki/index.php?title=Gramps_6.0_Wiki_Manual_-_Keybindings from the WIKI_KEYBINDINGS constant in gramps/gen/const.py#L83), perhaps that could link to the customization interface which has a Help button pointing to that page?

<attribute name="action">win.KeyBindings</attribute>

Reaching a given tab in a primary-object editor (Person, Family, Event,
etc.) previously required repeatedly tabbing through every preceding
widget or clicking with the mouse; the only keyboard alternative was
stepping one tab at a time with Alt+Left/Alt+Right. This adds direct,
positional tab-jump shortcuts, registered like the existing dialog-ok/
dialog-cancel actions so they show up in the Keyboard Shortcuts editor
and can be rebound or themed. Since it's wired once in EditPrimary's
shared _setup_notebook_tabs, it applies to every editor automatically.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants