Bundles
Buttons, stacks and pulldowns
The three things a panel holds most often, what each one costs you in space, and nine narrower kinds for when a plain button is not quite the shape you need.
A panel has room for a handful of large buttons before it starts pushing other panels off the ribbon. Stacks and pulldowns exist to buy that space back: a stack trades icon size for three tools in one column, a pulldown trades one click for an unlimited menu.
Pushbuttons, stacks and pulldowns are folders under a *.panel and the three
you will use for almost everything. A panel also understands nine narrower suffixes:
*.nobutton for a keyboard-only command, *.urlbutton and
*.linkbutton for pointing at something outside your own script, *.toggle
for on/off state, *.smartbutton for a button that configures its own appearance,
*.splitbutton/*.splitpushbutton for a pulldown whose header also
runs a child, *.dockpane for a real Navisworks dock panel instead of a script
that runs and returns, and *.slideout for the tools that belong in the panel's
flyout rather than on its face. More button kinds covers all
nine; a folder inside a panel whose name has a dot but is not one of these twelve suffixes is
logged as a typo and counted into the N bundle folders were skipped toast pyNavis
shows after every scan.
The three panel items#
*.pushbutton | *.stack | *.pulldown | |
|---|---|---|---|
| Renders as | One large button | 2 or 3 small buttons in one column | One large button that opens a menu |
| Children | None | Exactly 2 or 3 *.pushbutton |
1 or more *.pushbutton |
| Has its own bundle.yaml | Yes | No, it is never read | Yes, but only title, tooltip, keytip |
| Has its own icon | Yes | No | Yes |
| Runs a script | Yes, script.py |
No, its children do | No, its children do |
| Gets a keytip | Yes | Its children do | Yes, its children do not |
| Icon slot used | Large (32 logical) | Small (16 logical) | Large for itself, small for every menu row |
| Panel width | One narrow column | One wide column | One narrow column |
What a panel looks like#
This is the shipped Memory panel: a large button, a three-row stack, a two-row stack and a pulldown with its menu open. Drawn roughly to scale, because the relative sizes are the whole reason to choose one over another.
Pushbuttons#
A *.pushbutton placed directly on a panel is a large button: 32-logical icon
on top, label underneath, one click runs script.py. It is the default and it is
what a tool should be unless you have a reason otherwise.
Memory.panel\
01_Memorize.pushbutton\
script.py # required
config.py # optional, Shift+Click
bundle.yaml # title, tooltip, engine, shortcut, keytip
icon.png # 96x96
icon.dark.png
icon.small.png # 32x32, used if this button ever moves into a stack
icon.small.dark.pngOnly script.py is required. Without it the folder is dropped and the reason is
logged, '<dir>' has no script.py - skipped., which is also one of the lines
that feeds the warning toast after a scan. A missing or misnamed script is still the first
thing to check when a button does not appear.
A top-level button uses the large slot, but a bundle you may later move into a stack or a pulldown will need the small art. Generating all four variants costs nothing and means reorganising a panel never degrades an icon. See Icons.
Stacks#
A *.stack renders as two or three small buttons filling one panel column. Each
row is a full button with its own script, icon, tooltip, shortcut and keytip; the stack itself
is only a container.
Memory.panel\
03_Set.stack\ # no script.py, no bundle.yaml, no icon
01_Add.pushbutton\
script.py
icon.small.png
02_Subtract.pushbutton\
script.py
icon.small.png
03_Intersect.pushbutton\
script.py
icon.small.pngThe parser never opens bundle.yaml inside a .stack folder and
never looks for an icon there. The folder name is not shown anywhere either: it exists purely
to group and to sort. Put your metadata on the children.
A stack collects *.pushbutton folders and nothing else. Any other
bundle-looking folder inside it is logged, '<dir>' is ignored: a stack holds only
*.pushbutton folders., so a toggle or a nested stack you dropped in there is not a
silent miss. Pulldowns and the two split buttons enforce the same rule with the same
message.
The 2-or-3 rule#
The underlying ribbon control holds two or three rows and nothing else. Rather than let it render wrong, pyNavis drops the stack at parse time and says so:
Stack 'D:\NavisTools\...\03_Set.stack' holds 1 button(s); a stack needs 2 or 3 - skipped.The count is of usable children, so a stack of three where one child lost its
script.py survives as a two-row stack, and a stack of two where one child broke
disappears entirely. That is the usual way a working stack vanishes after an edit.
03_Set.stack\ 01_Add.pushbutton\ 02_Subtract.pushbutton\ 03_Intersect.pushbutton\
03_Set.stack\ 01_Add.pushbutton\ 02_Subtract.pushbutton\ 03_Intersect.pushbutton\ 04_Invert.pushbutton\ # 4 rows: the whole stack is dropped 05_Grow.stack\ # nested stacks are logged and ignored
When to use a stack#
- Two or three tools that belong to one idea and are used at similar frequency, such as Add, Subtract, Intersect.
- Tools whose meaning survives a 16-pixel icon and a short label. A stack row is horizontal, so a long title makes the column wide and eats the space you were trying to save.
- Never for a single tool: a one-button stack does not render.
Pulldowns#
A *.pulldown is a large button with no default action. Clicking it opens a
menu of its children. It is the only item that scales past three tools without consuming panel
width, so it is where the long tail goes.
Memory.panel\
05_Memory.pulldown\
bundle.yaml # title, tooltip, keytip only
icon.png # 96x96, the face of the button
icon.dark.png
icon.small.png # 32x32
icon.small.dark.png
01_Show_Contents.pushbutton\
script.py
02_Save_as_Set.pushbutton\
script.py
03_Purge.pushbutton\
script.pytitle: Memory
tooltip: Inspect the memory, promote it to a set, or delete stored memory filesWhat a pulldown's bundle.yaml can say#
| Key | On a pulldown |
|---|---|
title | Read. Falls back to the folder name with the prefix stripped. |
tooltip | Read. There is no docstring to fall back to, so without this key the button has no tooltip. |
keytip | Read. Overrides the assigned Alt-navigation letters. |
shortcut | Ignored. A pulldown has no action to trigger, so there is nothing for a chord to run. |
engine | Ignored. A pulldown runs no code. Put engine: on the children. |
The children are ordinary pushbuttons and lose nothing by being in a menu. Each may declare
its own engine, its own shortcut, its own config.py, and
each responds to Shift+Click and Alt+Click exactly as a top-level button
does.
Alt-navigation assigns letters to top-level buttons, stack rows and the pulldown itself, but
not to a pulldown's children: once the menu is open you arrow through it. If a buried tool
needs to be reachable from the keyboard, give it a shortcut: instead.
Shortcuts explains both mechanisms.
The empty-pulldown rule#
One usable child is enough. Zero is not:
Pulldown 'D:\NavisTools\...\05_Memory.pulldown' has no usable pushbuttons - skipped.A pulldown folder that contains a .stack or another .pulldown and
nothing else counts as empty: only *.pushbutton children are collected, and each
nested container is logged as '<dir>' is ignored: a pulldown holds only
*.pushbutton folders. before the pulldown itself is dropped for being empty.
More button kinds#
Nine more panel-item suffixes, each a narrow variation on a pushbutton, a pulldown or the panel itself rather than a new concept. All nine sort into the panel by folder name exactly like the three above, and every icon rule from Icons applies unless a section below says otherwise.
nobutton: a command with no ribbon presence#
A *.nobutton is parsed exactly like a pushbutton, script.py and
bundle.yaml and all, but the ribbon never draws it. It exists to be reached only
by a keyboard shortcut: or a user binding from the Shortcuts editor.
Utility.panel\
Rebuild_Index.nobutton\
script.py
bundle.yaml # give it a shortcut, since there is no button to clicktitle: Rebuild index
tooltip: Rebuilds the property index this session caches.
shortcut: Ctrl+Alt+REverything else about it is an ordinary bundle: the same bundle.yaml keys, the
same min_host_version/max_host_version gating, the same engine
resolution. It still counts toward the tools listed for shortcut binding, so it appears in the
Shortcuts editor beside every ordinary button.
Two things do not carry over, because both need a control to act on. context:
is parsed but never evaluated: greying is applied to rendered buttons, and a nobutton is never
rendered, so a chord fires it whatever the document holds. And there is no way to reach
config.py, because Shift+Click needs a button and a keyboard chord
always runs the primary script. If a nobutton needs options, give them their own bundle.
A nobutton with no author shortcut: still loads. Without one, and without a
user override added in the Shortcuts editor, there is no way to run it at all.
urlbutton: opens a link#
A *.urlbutton ships no script.py. Its whole job is a
url: key in bundle.yaml; clicking it opens that address in the
default browser and runs nothing else.
Help.panel\
Documentation.urlbutton\
bundle.yaml
icon.pngtitle: Documentation
url: https://example.com/docsIf the address cannot be opened, the failure is logged and toasted rather than raised into the ribbon. bundle.yaml has the full key table.
linkbutton: runs another Navisworks plugin#
A *.linkbutton also ships no script. Its required key is plugin:,
the target add-in's plugin id in Id.DeveloperId form. Use it to put someone
else's plugin on your own panel instead of sending users hunting through the Add-Ins tab.
title: Legacy Tool
plugin: LegacyTool.ACMEA plugin id that is not registered, or that resolves to something other than an add-in plugin, toasts a warning naming the id instead of failing silently. bundle.yaml has the full key table.
toggle: a button with on/off state#
A *.toggle is a pushbutton that also tracks a session-scoped boolean and swaps
its icon to match. script.py and bundle.yaml work exactly as they do
on a plain pushbutton; the additions are two extra icon pairs and two functions in
pynavis.script.
Snap.toggle\
script.py
bundle.yaml
icon.on.png # 96x96, shown while the toggle is on
icon.on.dark.png
icon.off.png # 96x96, shown while the toggle is off, and at startup
icon.off.dark.pngtitle: Grid snap
tooltip: Turns grid snapping on and off for this session.
shortcut: Ctrl+Shift+G"""Turns grid snapping on and off."""
from pynavis import script, toast
on = not script.get_toggle_state()
script.set_toggle_state(on)
toast.info('Snap ' + ('on' if on else 'off'))| Icon file | Falls back to |
|---|---|
icon.off.png | icon.png |
icon.off.dark.png | icon.off.png |
icon.on.png | icon.png |
icon.on.dark.png | icon.on.png |
Every toggle starts off. script.get_toggle_state() reads the current state;
script.set_toggle_state(on) sets it and repaints the icon immediately.
Toggle state lives in memory for the running session, keyed by the bundle's own path. It
survives Reload, because Reload rebuilds the ribbon without resetting state, but it does not
survive a restart, and there is no way to set it from anywhere except the toggle's own
script.py or config.py: get_toggle_state and
set_toggle_state act on whichever bundle is currently running, and neither a hook
nor startup.py runs as a particular bundle. A toggle that must come back on after
a restart needs its own pynavis.settings entry, read by its own
script.py on the first click of a new session.
smartbutton: sets its own title, tooltip and icon at build time#
A *.smartbutton runs script.py twice: once while the ribbon is
being built, so the script can configure its own appearance before anyone sees it, and once
per click, exactly like a pushbutton. A global, __selfinit__, tells the script
which run it is in.
if __selfinit__:
__button__.Title = 'Clash (0)'
__button__.Enabled = True
else:
from pynavis import toast
toast.info('Clicked!')On the build-time run, __selfinit__ is True and
__button__ is a small proxy over the live ribbon control:
| Member | Does |
|---|---|
__button__.Title | Read/write. The button caption. |
__button__.Enabled | Read/write. Greys the button out when set to False. |
__button__.Tooltip | Write-only. The hover tooltip. |
__button__.SetIcon(path) | Points both the light and dark icon slots at the given PNG. |
On a normal click, __selfinit__ is False and
__button__ does not exist at all: guard on __selfinit__, exactly as
the example does, or a click-time run raises NameError.
The build-time run's scope is smaller than a click's in the other direction too: it gets
only __file__, __selfinit__, __button__ and
__pynavis__, so __commandpath__ and __title__ do not
exist there. A script that touches either outside the click branch works on every click and
then fails its selfinit run. Derive the bundle folder from __file__ in the
selfinit branch instead; pynavis.script.get_command_path() is no substitute,
because a build-time run sets no command context and would read back whichever command ran
last. Anything a selfinit run prints lands in the log, labelled
[selfinit <bundle key>], never in an output window.
The build-time run fires while the ribbon is under construction, before the button is clickable, and nothing repeats it until the next Reload rebuilds the tab. It is the place to set an initial appearance from saved state, not to poll a value that changes while pyNavis is running; drive that from a hook instead. A selfinit run that raises is logged, and the button still renders with its folder-derived title and still runs normal clicks.
splitbutton and splitpushbutton: a pulldown whose header also runs a child#
Both are pulldowns in every other respect: a folder of *.pushbutton children,
their own bundle.yaml reading title, tooltip and
keytip, their own icon set. The only difference from a plain
*.pulldown is what clicking the large header itself does.
*.pulldown | *.splitbutton | *.splitpushbutton | |
|---|---|---|---|
| Clicking the header | Opens the menu | Runs the last child that was run, this session | Always runs the first child, by folder order |
| Menu arrow | Only way in | Opens the menu, same as a plain pulldown | Opens the menu, same as a plain pulldown |
Export.panel\
01_Points.splitbutton\
bundle.yaml # title, tooltip, keytip
icon.png
01_CSV.pushbutton\
script.py
02_IFC.pushbutton\
script.pytitle: Export points
tooltip: Exports the selection. The arrow picks the format.
keytip: EPA splitbutton with nothing run yet this session falls back to the first child, the same as a splitpushbutton always does, so the very first click always has a defined target.
dockpane: a real Navisworks dock panel#
A *.dockpane is not a script that runs and returns: it opens a real Navisworks
dock panel whose content is pane.xaml, and its ribbon button is a toggle that
follows whether the panel is open, not a button you click to fire an action. It follows in
both directions and at all times: Navisworks closing the panel itself (its close button, a
workspace change) releases the toggle, and a Reload that rebuilds the ribbon while the panel
stays open seeds the new button already pressed.
Live_View.dockpane\
pane.xaml # required - a plain element, not a <Window>
script.py # optional, runs once when the panel's content is built
bundle.yaml # optional
icon.png, icon.dark.png, icon.small.png, icon.small.dark.png
icon.on.png, icon.on.dark.png # optional - the pressed state while the panel is opentitle: Live view
tooltip: A docked summary of the current selection.
engine: ironpython
keytip: LVIf script.py exists it runs once, when the panel's content is built, with the
same five globals as any other bundle plus a sixth, __pane__:
| Member | Does |
|---|---|
__pane__.Find(name) |
The named element from pane.xaml, or None. |
__pane__.Content |
The root element pane.xaml produced. |
__pane__.Title |
The panel's resolved title, read-only. |
__pane__.BundleKey |
The panel's own bundle key, for calling pynavis.panes on itself. |
__pane__.Visible |
Read/write. Shows or hides the panel. |
from pynavis import selection
label = __pane__.Find('Summary')
label.Text = 'Selected: %d' % len(selection.get_items())pynavis.panes resolves a missing bundle_key to whichever pyNavis
command most recently ran, which is correct while script.py itself is running but
wrong from inside an event handler wired up in it (a button's Click, a timer):
the user may have clicked other tools by the time it fires. From inside a panel's own event
handler, pass the bundle key explicitly, or read/write __pane__.Visible
instead.
There is no config.py, no Shift+Click and no Alt+Click-to-config for a
dockpane. Alt+Click still opens the bundle folder; a plain click toggles the panel open and
shut instead of running a script that returns.
Five panel slots ship with pyNavis, and a new *.dockpane bundle claims a free
one on the next Reload, the same as any other new bundle. The claim is recorded in
config.json under panes.assignments and kept for life: Navisworks
remembers a dock pane's position, size and open state per slot, so holding the same slot is
what lets your panel come back where the user docked it, and lets a reinstalled extension pick
its old slot and layout straight back up.
A claim is keyed by the bundle's path relative to its own extension folder, so
two different extensions can produce the identical key: a
Tools.tab/Main.panel/Live_View.dockpane in each of two installed extensions
collides. The runtime cannot tell the two claims apart; it logs
Two dockpane bundles share the key '<key>' - their extensions will cross slot
ownership. Bundle keys must be unique. and the two panes end up fighting over one slot
and one saved dock layout. Name your tab, panel or dockpane folders distinctively enough that
the relative path is yours alone.
Claiming a slot is instant; minting more of them (via the Panel slots
button on the pyNavis tab, or pynavis.panes.add_slots(count)) needs a restart,
because Navisworks only discovers dock panels by scanning types at startup. Until that restart
the count can be changed again freely; once the generated assembly has loaded, it is locked
for every later session, and changing the count then means closing Navisworks and deleting
PyNavisPanes.dll from the pyNavis bundle by hand, which is what the error tells
you. A
dockpane that loses the scramble for a slot still renders its ribbon toggle; clicking it
toasts instead of doing nothing, and the toast tells apart genuine overflow (every slot
claimed; add more, then restart) from a slot minted this session that just needs the restart
it already announced.
bundle.yaml has the full key table.
The panel's title bar shows your bundle's title:, but Navisworks'
View > Windows menu always lists the slot name, "pyNavis Panel N", because
that menu reads the docking library's own labels rather than the window caption. The same menu
lists every slot whether a bundle has claimed it or not, so a clean install shows five entries
and generating extra slots adds more; opening an unclaimed one gives a panel reading "Panel
slot N is not in use". Neither is something a bundle can change.
slideout: the tools behind the panel title#
A *.slideout is the one panel item that is not a button at all and has no
bundle of its own. It is a marker folder: everything inside it is emitted after a panel break,
so the host hides it until the user clicks the panel title along the bottom of the panel, the
same flyout the native Tags panel uses. It costs no ribbon width, which is the point.
Export.panel\
01_Points.pushbutton\ # on the face of the panel
02_Mesh.pushbutton\
99_More.slideout\ # no bundle.yaml, no icon, no script.py
01_Settings.pushbutton\
script.py
02_Formats.pulldown\
bundle.yaml
01_CSV.pushbutton\
script.py
03_Docs.urlbutton\
bundle.yaml| Behaviour | |
|---|---|
| Children | Any of the other panel item kinds, not just *.pushbutton. Stacks,
pulldowns, toggles, smartbuttons, split buttons, and url and link buttons all work
inside one. A *.dockpane is the one exception, see below. |
| Its own bundle.yaml | No, it is never read. Nor is an icon, nor a script.py. |
| Title | Never shown. The flyout is labelled by the panel, not by the slideout folder. |
| Ordering | Its children keep folder order inside the flyout. Where the .slideout
folder itself sorts among the panel's other folders does not matter, because its
contents always render after the break. |
| Several per panel | Allowed. Their contents concatenate in folder order into one flyout; a panel has only one break. |
| Nesting | One level. A .slideout inside a .slideout is logged and its
contents are dropped. |
Slideout 'D:\NavisTools\...\99_More.slideout\Extra.slideout' sits inside another slideout - skipped.Buttons in the flyout are ordinary buttons. They get keytips, they take
shortcut:, context: and the host version keys, and
Shift+Click and Alt+Click behave exactly as they do on the panel face.
Their bundle key keeps the .slideout folder in the
path, so moving a tool into or out of a slideout orphans any user shortcut override that
pointed at it.
If every item on a panel sits inside a .slideout, the panel still renders: it
just has an empty face and a full flyout. That is a reasonable shape for a panel of rarely
used maintenance tools that you still want grouped under their own name.
A *.dockpane inside a .slideout claims a panel slot exactly like
one on the face of the panel; only its ribbon toggle moves into the flyout. Its bundle key
includes the slideout folder, so T.tab/P.panel/More.slideout/S.dockpane is the
key you will see under panes.assignments.
Mixing them on one panel#
Panel items are sorted once, by folder name, before anything knows what type they are. The three types interleave freely and are never grouped, so the panel order is exactly the order you see in Explorer.
Icons at each size#
The size an item renders at decides which icon it asks for, which is why the four-variant icon set exists.
| Where it renders | Icon file used | Drawn at |
|---|---|---|
| Large pushbutton on a panel | icon.png / icon.dark.png | 32 logical pixels |
| Row in a stack | icon.small.png / icon.small.dark.png | 16 logical pixels |
| The pulldown button itself | icon.png / icon.dark.png | 32 logical pixels |
| Row in a pulldown menu | icon.small.png / icon.small.dark.png | 16 logical pixels |
Every missing variant falls back to icon.png, so a bundle that ships only the
one file still renders everywhere. It is loaded at the slot's logical size rather than being
scaled from the large art, which keeps it from clipping, but a 96×96 drawing rarely reads
well at 16 pixels. A menu of pulldown rows is where that shows worst.
Whether the light or dark art is used is decided once, when the ribbon is built. If you change the Navisworks theme, click Reload to pick up the other set.
Choosing between the three#
| Situation | Use |
|---|---|
| A tool people reach for every day | A large pushbutton, at the left of the panel |
| Two or three variations on one action | A stack |
| Four or more related tools | A pulldown |
| One rarely used tool | A pulldown row, or the far right of a panel |
| A tool with a long name | A pulldown row: menus have horizontal room, stacks do not |
| Settings for an existing tool | Not a button at all: add config.py and let
Shift+Click open it. See Click actions. |
Identity on the ribbon#
Each rendered control gets a deterministic id, which is worth knowing when you read a log line or wonder why a duplicate got dropped.
| Control | Id | Derived from |
|---|---|---|
| Button | PYNAVIS_BTN_<hash> | The lowercased full path of its script.py |
| Pulldown | PYNAVIS_PULL_<hash> | The lowercased pulldown folder path |
| Dock pane toggle | PYNAVIS_PANE_<hash> | The lowercased dockpane folder path |
| Tab | PYNAVIS_TAB_<name> | Extension name plus tab name, sanitised. See Anatomy. |
Because button ids come from the script path, the same bundle copied to two extension roots produces two different ids and renders twice. Tab ids come from names, so the duplicate tab is caught and dropped. That asymmetry is why an accidentally duplicated extension usually shows up as a missing tab rather than a doubled one.
Common mistakes#
- A stack with four children. Split it into two stacks, or promote it to a pulldown.
bundle.yamlinside a.stackfolder. Never read. Move the keys down onto the children.- An icon inside a
.stackfolder. Never read either. shortcut:on a pulldown. Ignored. Put it on the child you actually want the chord to run.- A
.pulldownnested inside a.pulldown. There are no submenus. Flatten it, or use a second pulldown on the panel. Put it in a*.slideoutinstead if the goal was to get it off the panel face. - A
.slideoutinside a.slideout. One level only; the inner one is logged and its contents never render. - Relying on a large icon in a menu. Draw the 32×32 variants for anything that lives in a stack or a pulldown.