Bundles
Shortcuts and keytips
Keyboard chords that run a tool without touching the ribbon, and the Alt letters that walk to it.
A pyNavis tool can be reached three ways: by clicking it, by pressing a keyboard chord such as Ctrl+Shift+M, or by walking to it with Alt and a couple of letters. The second and third are what this page is about. Both are cheap to add and both are rebuilt from scratch on every boot and every Reload, so you can iterate on them as fast as you iterate on the script.
Two places a binding comes from#
You, the tool author, declare a default in bundle.yaml. The person using your
tool overrides it in config.json. Neither knows about the other, and the user
always wins.
title: Memorize
tooltip: Stores the current selection in the memory register.
shortcut: Ctrl+Shift+M
keytip: MM{
"shortcuts": {
"allowBareKeys": false,
"bindings": {
"pyNavis.tab/Memory.panel/01_Memorize.pushbutton": "Ctrl+Alt+M",
"pyNavis.tab/Memory.panel/02_Recall.pushbutton": null
}
}
}The first entry rebinds Memorize. The second disables Recall entirely: null
means unbound, not "use the default". Users rarely write this file by hand, because the
built-in Shortcuts editor on the pyNavis panel writes it for them, but the
format is plain and worth knowing.
Navisworks and Windows already own a lot of the chord space. The shipped tools use Ctrl+Shift+letter, which is mostly free. Avoid plain Ctrl+letter for anything a user would expect to save, copy or print, and remember that your default is only a suggestion: anyone can move it.
The chord grammar#
A binding string is split on +, each token is trimmed, and every comparison is
case-insensitive. ctrl + shift + m parses exactly like Ctrl+Shift+M.
The vocabulary is deliberately small.
| Token group | Accepted | Not accepted |
|---|---|---|
| Modifiers | Ctrl, Alt, Shift |
Win, Control, Meta, Cmd |
| Letters and digits | A to Z, 0 to 9 |
Punctuation of any kind |
| Function keys | F1 to F24 |
|
| Arrows | Left, Up, Right, Down |
|
| Navigation | Home, End, PgUp, PgDn |
PageUp, PageDown |
| Everything else | Nothing | Esc, Tab, Space, Enter,
Del, Ins, numpad keys |
Exactly one key token is required, and no more than one. A chord may carry any combination of the three modifiers, subject to the policy below.
| What you wrote | Logged error |
|---|---|
"" or whitespace | empty binding |
Ctrl+M+K |
'Ctrl+M+K': more than one key |
Ctrl+Esc |
'Ctrl+Esc': unknown token 'Esc' |
Ctrl+Shift |
'Ctrl+Shift': no key |
Shift+M |
'Shift+M': bindings need Ctrl or Alt (set shortcuts.allowBareKeys to permit bare
keys) |
The Ctrl-or-Alt policy#
A binding must contain Ctrl or Alt. Shift+M and a
bare M are both rejected, because a chord that is a single printable key would
fire every time someone typed into a control the runtime failed to recognise as a text
field.
A user who wants that anyway can opt in:
{ "shortcuts": { "allowBareKeys": true } }The flag is read only when it is a real JSON boolean; a string "true" is
ignored and the default of false stands. It is a user setting, not a bundle
setting: as an author you cannot enable it for your own tool, so never ship a bare-key
default.
How the map is built#
Before each ribbon build, every pushbutton-shaped bundle in every extension is walked,
defaults and overrides are merged, and the result is one table of chord to button.
Pushbutton-shaped means anything that runs a script.py on a click:
*.pushbutton wherever it sits, plus *.nobutton,
*.toggle and *.smartbutton. The other shapes take no chord:
a urlbutton or linkbutton has no script to run, and a dockpane's shortcut: is
parsed but reserved, not wired to the toggle in this release. The order of the walk is
what breaks ties, so it is worth stating: extensions, then tabs, then panels, then the buttons
of a panel in order, with stack and pulldown children flattened in place, and a panel's
*.slideout contents last, after everything on its face. Duplicate
BundleKey values dedupe, first occurrence wins.
The rules, in the order they apply:
- An override keyed to a tool that does not exist logs
shortcut override for unknown tool '<key>' ignoredand is dropped. This is what happens after you rename a bundle folder. - For each button, a user entry replaces the author default entirely.
A
nullvalue means unbound; the default is not consulted. - The chosen string is parsed. A failure logs
'<BundleKey>': <error>and the binding is skipped, leaving the tool unbound. - If the parsed chord is already taken, the later button loses and logs
'<key>' wants <chord> but '<winner>' already has it - first wins. First in ribbon order keeps the chord. - All problems are logged with the prefix
Shortcut:, followed by a summary line:Shortcuts: N binding(s) active.
There is a deliberate asymmetry here. When a user's chord string fails to parse, the runtime
skips it and the tool is unbound, but the Shortcuts editor dialog falls back
to displaying the author default for that row. The dialog shows what the tool would
have; the log shows what it actually has. If a shortcut is not firing, read
%APPDATA%\pyNavis\logs\pyNavis.log and search for Shortcut:.
The BundleKey#
Overrides are keyed by BundleKey: the bundle's folder path relative to the
.extension folder that contains it, with forward slashes. Nothing is stripped.
Folder suffixes stay, and so do the NN_ ordering prefixes that the ribbon
removes from the visible title.
.extension folder, so it survives
moving the extension to another drive but not renaming a folder inside it.A button nested inside a stack or a pulldown keeps those folders in its key:
pyNavis.tab/Memory.panel/03_Set.stack/01_Add.pushbutton
pyNavis.tab/Memory.panel/05_Memory.pulldown/03_Purge.pushbuttonOpen the Shortcuts editor, set the binding there, and let it write the
key. Renaming a bundle folder invalidates every override that points at it, and the only
symptom is the unknown tool line in the log.
When a chord fires#
Bindings are watched by a keyboard hook, and a matched chord still has to clear three guards before anything runs.
The guards in detail:
- Fresh key down only. Holding the chord down does not run the tool repeatedly; auto-repeat is suppressed.
- Navisworks must be foreground. The chord does nothing while another application has focus, even though the hook is still installed.
- Focus must not be a text input. A WPF
TextBoxBaseorPasswordBoxblocks dispatch, as does a native window whose class name contains "edit". If focus cannot be determined at all, the runtime assumes you are typing and does not fire. The safe answer is always "do nothing".
The hook is a thread-scoped WH_KEYBOARD hook on the Navisworks UI thread. It
sees keys delivered to that thread and nothing else. pyNavis does not install a system-wide
keyboard hook and cannot see what you type in other applications.
Ctrl+Shift+M runs script.py, not
config.py, even though the chord contains Shift. Keyboard chords and modifier
click actions are separate mechanisms: Shift+Click is the only way to reach the
secondary action. See Click actions.
A resolved binding is appended to the button's tooltip in canonical form, which is always
Ctrl+Alt+Shift+Key in that order regardless of how it was spelled in the file.
shift+ctrl+m and Ctrl+Shift+M both display as
Ctrl+Shift+M.
Keytips#
Keytips are the letters that appear over the ribbon when you press and release
Alt. They are assigned automatically, per tab, and you can steer the result with a
keytip: line in bundle.yaml.
Assignment covers everything that renders on the tab: top-level pushbuttons, the child
buttons of a stack, the pulldown menu button itself (and the same for a
*.splitbutton and a *.splitpushbutton), urlbuttons, linkbuttons, and
the ribbon toggle of a dockpane. Items inside a *.slideout are included too, and
compete for letters with everything else on the tab. Pulldown children get none,
because an open menu is navigated with the arrow keys, and a *.nobutton gets none
because it never renders.
Assignment runs in two passes over the items of one tab:
- Explicit values claim first. Every
keytip:frombundle.yamlis trimmed and uppercased, and takes its letters. Content is not validated: any string is accepted as written. - Everything else is derived from the title. Non-letters are dropped and the rest uppercased, then the candidates above are tried in order and the first untaken one wins. A title with no letters at all gets nothing.
If two bundles on the same tab both declare keytip: MM, the first one claims it
and the second gets no keytip at all. It does not fall through to
auto-assignment. Declaring a keytip is a commitment to it being unique on that tab.
The tab keytip itself is hard-coded to PY and nothing dedupes it. If two
extensions each create a tab, both claim PY and Alt navigation into them becomes
unreliable. Until this is fixed, put your buttons on the existing tab or accept the
collision.
The Shortcuts editor#
The Shortcuts button on the pyNavis panel opens the editor: every tool, its author default, and its current binding. Recording a chord is a matter of clicking a row and pressing the keys.
| Key | While recording |
|---|---|
| Esc | Leaves the field, changing nothing. |
| Backspace or Delete | Clears the row, which means disabled. This is what writes a null. |
| Tab | Always passes through, so the dialog stays keyboard navigable. |
| Modifiers alone | Keep recording; a chord is only captured once a real key arrives. |
Every capture is round-tripped through the same parser the runtime uses, so the recorder physically cannot produce a binding the runtime would reject. An unsupported key shows unsupported key, and a chord without Ctrl or Alt shows needs Ctrl or Alt.
Saving writes a minimal map: only the rows that differ from their author
default are stored, stale keys are dropped, and every other key in
config.json is preserved. If nothing differs, the bindings key is
removed, and shortcuts goes with it when nothing else is left under it. A
malformed config logs Failed to load config '<path>' - using defaults. and
unknown keys are ignored rather than rewritten away.
Checklist#
- Author defaults use Ctrl+Shift+letter unless you have a reason not to.
- Every binding contains
CtrlorAlt; never ship a bare-key default. - Key tokens come from the supported list only: letters, digits,
F1toF24, arrows,Home,End,PgUp,PgDn. - Check the log after a Reload for lines beginning
Shortcut:and for theN binding(s) activesummary. - Remember that a duplicate chord is resolved by ribbon order, so the tool nearest the start of the tab wins.
- Declare
keytip:only when you are sure it is unique on the tab; otherwise let the automatic pass do it. - Overrides are keyed by folder path, so renaming a bundle silently orphans them.