Fusion Guide
advanced10 min read

Building UI: Commands, Toolbars and Palettes

Create command definitions, add buttons to toolbar panels, build command dialogs with inputs, and show HTML palettes that talk to Python.

Overview#

A script that opens a message box is a start, but a real tool lives in the interface: it has a button on a toolbar, opens a dialog with inputs, previews its result, and can leave a palette docked beside the canvas. In Fusion this is built from three cooperating ideas:

  • A command definition (CommandDefinition) is the reusable description of a command: its id, name, tooltip, and icon. It is what a button points to.
  • A control (CommandControl) is a button, drop-down, or separator placed in a toolbar panel or menu and pointing to a command definition.
  • A command (Command) is a running instance, created when the user clicks the button. It owns a command dialog with command inputs, and fires events through its lifetime: created, input changed, preview, execute, destroy.

For web-style interfaces there is a fourth idea, the palette (Palette), a dockable window that hosts an HTML page, which communicates with your Python code through messages.

Persistent UI belongs in add-ins, because the button must exist after run returns. The event handling concepts used here are expanded on the next page.

Where to find it#

In the API reference: UserInterface, CommandDefinitions, CommandDefinition, Command, CommandCreatedEventArgs, CommandInputs, the individual input classes (ValueCommandInput, DropDownCommandInput, SelectionCommandInput, BoolValueCommandInput, StringValueCommandInput, SliderCommandInput, TextBoxCommandInput, TableCommandInput, TabCommandInput, GroupCommandInput, ImageCommandInput, ButtonRowCommandInput, DirectionCommandInput, DistanceValueCommandInput, AngleValueCommandInput, BrowserCommandInput, RadioButtonGroupCommandInput), Workspaces, ToolbarPanels, ToolbarControls, Palettes, and Palette.

In the Fusion interface, your button appears wherever you place it, typically in one of the Design workspace's panels such as SOLID > CREATE (SolidCreatePanel) or the ADD-INS panel (SolidScriptsAddinsPanel). Autodesk's guidance is to put a command where a user would logically look for similar functionality, and to add new tabs or panels only when the command is unique, because there is limited room.

Step by step#

1. Create a command definition#

import adsk.core, adsk.fusion, traceback

CMD_ID = 'myCompany_MyTool_cmd'
CMD_NAME = 'My Tool'
CMD_DESC = 'Creates a custom thing'

app = adsk.core.Application.get()
ui = app.userInterface

cmd_def = ui.commandDefinitions.itemById(CMD_ID)
if not cmd_def:
    cmd_def = ui.commandDefinitions.addButtonDefinition(
        CMD_ID, CMD_NAME, CMD_DESC, './resources')   # folder with 16x16/32x32 icons

addButtonDefinition(id, name, tooltip, resourceFolder) is the usual constructor. The resource folder (a path relative to the add-in's main .py, such as ./Resources/Sample) holds the images: icons are 16x16 (small) and 32x32 (large), as PNG or SVG (SVG preferred), with @2x variants for high-DPI displays (16x16@2x.png) and theme tokens -light_gray, -dark_blue and -normal (32x32-dark_blue.svg). A missing icon is replaced by a placeholder with a question mark. The other command-definition types are addCheckBoxDefinition and addListDefinition; there is no addSplitButtonDefinition. Split buttons, drop-downs and separators are controls, created with ToolbarControls.addSplitButton, addDropDown and addSeparator.

Always check itemById first. If an earlier run of your add-in crashed before cleaning up, a duplicate id throws an error.

2. Find a toolbar panel and add a control#

workspace = ui.workspaces.itemById('FusionSolidEnvironment')   # the Design workspace
panel = workspace.toolbarPanels.itemById('SolidCreatePanel')
control = panel.controls.addCommand(cmd_def)
control.isPromoted = True            # also show on the main toolbar row
control.isPromotedByDefault = True

Ids confirmed in Autodesk's documentation and samples: FusionSolidEnvironment (the DESIGN workspace), FusionRenderEnvironment, CAMEnvironment, the toolbar tab SolidTab, the panels SolidCreatePanel and SolidScriptsAddinsPanel, the toolbars QAT, QATRight and NavToolbar, and the commands ScriptsManagerCommand, EmbossCmd and FileSubMenuCommand. Autodesk says a complete list of UI ids is not published because add-ins can change the UI; use the "Write user interface to a file" sample, which exports the full UI structure (workspaces, tabs, panels, controls with their ids) to an XML file. > Note: not confirmed in Autodesk's documentation: ids such as SimulationEnvironment, PCBEnvironment, FusionDocumentationEnvironment, SolidModifyPanel and InspectPanel that earlier drafts of this page listed; read them from the exported XML.

addCommand(cmdDef, positionID, isBefore) places a control relative to an existing one, for example createPanel.controls.addCommand(cmd_def, 'EmbossCmd', False) puts it after the Emboss command. Panels are unique within a workspace, so workspace id plus panel id is enough; you can also reach a panel through its tab (designWS.toolbarTabs.itemById('SolidTab').toolbarPanels...). Fusion does not remember UI edits, so an add-in must re-create its controls on every start.

To create your own panel and tab so you do not mix with Autodesk's:

panel_id = 'myCompany_MyTool_panel'
panel = workspace.toolbarPanels.itemById(panel_id)
if not panel:
    panel = workspace.toolbarPanels.add(panel_id, 'My Tools')

The API also has ToolbarTabs.add for custom toolbar tabs, with panels added to the tab through tab.toolbarPanels.

3. Connect the command-created handler#

Clicking the button fires commandCreated on the definition. You respond by building the dialog.

handlers = []     # module-level list keeps handler objects alive

class CreatedHandler(adsk.core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:
            cmd = args.command
            inputs = cmd.commandInputs

            inputs.addStringValueInput('name', 'Name', 'Part')
            inputs.addValueInput('size', 'Size', 'mm',
                                 adsk.core.ValueInput.createByString('20 mm'))
            dd = inputs.addDropDownCommandInput('shape', 'Shape',
                                                adsk.core.DropDownStyles.TextListDropDownStyle)
            dd.listItems.add('Box', True)
            dd.listItems.add('Cylinder', False)
            sel = inputs.addSelectionInput('face', 'Face', 'Select a planar face')
            sel.addSelectionFilter('PlanarFaces')
            sel.setSelectionLimits(0, 1)

            on_exec = ExecuteHandler()
            cmd.execute.add(on_exec)
            handlers.append(on_exec)
        except:
            ui.messageBox('Created failed:\n{}'.format(traceback.format_exc()))

class ExecuteHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:
            cmd = args.command
            inputs = cmd.commandInputs
            name = inputs.itemById('name').value
            size_cm = inputs.itemById('size').value          # already in internal units (cm)
            shape = inputs.itemById('shape').selectedItem.name
            app.log(f'{name}: {shape} {size_cm} cm')
        except:
            ui.messageBox('Execute failed:\n{}'.format(traceback.format_exc()))

on_created = CreatedHandler()
cmd_def.commandCreated.add(on_created)
handlers.append(on_created)

Three details recur in every UI add-in:

  • Handler classes subclass specific event handler base classes (CommandCreatedEventHandler, CommandEventHandler, InputChangedEventHandler, ValidateInputsEventHandler, and so on) and override notify(self, args).
  • Handlers must be kept alive in a module-level list ("only needed with Python", per Autodesk's samples), or Python can garbage collect them and the event stops firing.
  • A ValueCommandInput with unit 'mm' returns .value in internal units (cm) and .expression as the text the user typed.

4. Run the command#

Pressing the button now shows a dialog with the inputs and OK/Cancel. OK fires execute. The standard lifecycle events on Command are:

EventFires when
commandCreated (on the definition)The user starts the command
activate / deactivateAfter the dialog is shown (pre-populate selection inputs here); also when a non-terminating command such as Pan or Orbit pauses yours
inputChangedAny input changes; do not modify the model here (it clears selections)
validateInputsSet args.areInputsValid; False disables OK
executePreviewEach input change, to show a preview in its own transaction
executeThe user presses OK; everything done here is a single undo
preSelect / selectSelection filtering (isSelectable = False blocks an entity)
mouseClick, keyDown, ...Mouse (click, double-click, down, drag, move, up, wheel) and keyboard (keyDown, keyUp) events while your command runs
destroyThe last event; fires however the command ends

For a live preview, create the geometry in executePreview and set args.isValidResult = True if you want OK to reuse the preview rather than run execute again; Fusion aborts the previous preview transaction each time the event fires.

5. Clean up in stop#

def stop(context):
    try:
        panel = ui.workspaces.itemById('FusionSolidEnvironment').toolbarPanels.itemById('SolidCreatePanel')
        ctrl = panel.controls.itemById(CMD_ID)
        if ctrl:
            ctrl.deleteMe()
        cd = ui.commandDefinitions.itemById(CMD_ID)
        if cd:
            cd.deleteMe()
    except:
        ui.messageBox('Stop failed:\n{}'.format(traceback.format_exc()))

Delete the control first, then the definition (Autodesk's UI topic does it in that order). If you do not, the commands stay in the UI for the session but are dead because nothing handles their commandCreated events.

Palettes#

A palette is an HTML page shown in a dockable window. Use palettes for rich displays: charts, forms, progress logs, or an embedded web app.

PALETTE_ID = 'myCompany_MyTool_palette'

def show_palette(html_path):
    palette = ui.palettes.itemById(PALETTE_ID)
    if not palette:
        palette = ui.palettes.add(
            PALETTE_ID, 'My Palette', html_path,
            True,    # isVisible
            True,    # showCloseButton
            True,    # isResizable
            300, 400,  # width, height
            True)    # useNewWebBrowser (optional; defaults to False, the older CEF browser)
        palette.dockingState = adsk.core.PaletteDockingStates.PaletteDockStateRight
        on_html = HTMLEventHandler()
        palette.incomingFromHTML.add(on_html)
        handlers.append(on_html)
    else:
        palette.isVisible = True
    return palette

class HTMLEventHandler(adsk.core.HTMLEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        html_args = adsk.core.HTMLEventArgs.cast(args)
        # html_args.action is a string, html_args.data is a string (often JSON)
        if html_args.action == 'ping':
            html_args.returnData = 'pong'
            palette = ui.palettes.itemById(PALETTE_ID)
            palette.sendInfoToHTML('fromFusion', '{"count": %d}' % app.activeDocument.products.count)

On the JavaScript side, the page calls adsk.fusionSendData(action, data) to send a string to Python, and defines window.fusionJavaScriptHandler with a handle(action, data) method to receive messages sent by sendInfoToHTML:

<script>
  window.fusionJavaScriptHandler = {
    handle: function (action, data) {
      if (action === 'fromFusion') {
        document.getElementById('out').textContent = data;
      }
      return 'OK';
    }
  };
  function ping() {
    adsk.fusionSendData('ping', '{}').then(function (r) { console.log(r); });
  }
</script>

Autodesk's Palettes topic explains the browser flag: the last argument selects the older CEF browser (False, the default) or the new Qt Web Browser (True). Fusion is moving to the Qt browser, CEF will eventually be removed, and Autodesk "highly recommends" passing True. The one known difference is adsk.fusionSendData: with CEF it is synchronous and returns the value you set in returnData; with the Qt browser it returns a Promise, as in the example above. The JavaScript handle function must return a non-empty string (the topic says an empty string is read as an error), and what it returns comes back as the return value of sendInfoToHTML.

Palette facts from the same topic: the HTML path can be absolute, relative to the add-in's .py, or a web URL; the palette id must be unique in the session; a palette is not tied to a document or a command; JavaScript in the palette cannot call the Fusion API; and Fusion deletes all palettes when the user switches workspaces, after which your Palette object reports isValid == False. Re-create it, or delete it yourself in a workspacePreActivate handler. Pass False for isVisible first if you want to set the position (palette.setPosition(x, y)) before showing it. The palette's browser developer tools are off by default and are enabled by a preference. Send only strings between the two worlds (serialise with json.dumps and parse in JavaScript). BrowserCommandInput hosts the same kind of HTML inside a command dialog.

Other UI elements#

  • Message boxes: ui.messageBox(text, title, buttonType, iconType), returning a DialogResults value.
  • Input box: ui.inputBox(prompt, title, default) returns (value, cancelled).
  • File dialogs: createFileDialog() and createFolderDialog().
  • Selection: ui.selectEntity(prompt, filter) for a quick pick in a script (not supported inside command events), with the filter string such as 'Faces', 'Edges', 'SolidBodies', 'Profiles', 'PlanarFaces', 'SketchCurves'. It returns a Selection; use SelectionCommandInput when you need more control.
  • Active selections: ui.activeSelections gives the current selection set.
  • Text Commands: ui.palettes.itemById('TextCommands') returns a TextCommandPalette; writeText('...') writes to it.
  • Progress: ui.createProgressDialog() and the Progress Dialog sample show a progress bar for long jobs.
  • Custom graphics: for visualising temporary geometry in the canvas, use component.customGraphicsGroups (see the Custom Graphics topic and sample).

Options reference#

ItemWhat it doesNotes
commandDefinitions.addButtonDefinitionCreates a button definitionNeeds unique id
toolbarPanels.add(id, name)New custom panelDelete in stop
controls.addCommand(def)Places a buttonisPromoted for main row
commandInputs.addValueInputNumeric input with units.value in cm
commandInputs.addSelectionInputPick geometryFilters by string
commandInputs.addDropDownCommandInputListStyles: TextListDropDownStyle, CheckBoxDropDownStyle, LabeledIconDropDownStyle
commandInputs.addTableCommandInputTableRows built from other inputs
cmd.setDialogInitialSize(w, h)Dialog sizePixels
cmd.isOKButtonVisibleHide OKUseful for info dialogs
ui.palettes.add(...)HTML paletteSee argument list above
palette.sendInfoToHTML(action, data)Python to JSStrings only

Tips and pitfalls#

Tip: Use args.inputs inside inputChanged to read the values and call inputs.itemById('x').isVisible = False to show or hide fields dynamically. Group related inputs in a GroupCommandInput so they collapse.

Tip: Namespace every id with your company or tool name. Autodesk's template builds ids as f'{COMPANY_NAME}_{ADDIN_NAME}_cmdDialog' and says the id must be unique among all commands, including Fusion's own.

Tip: The add-in that the Create dialog generates is more structured than this page's single-file examples: a commands folder with one subfolder per command (entry.py plus resources), a lib folder with fusion360utils (an event helper, futil.add_handler), and config.py holding DEBUG and COMPANY_NAME. It adds three commands to the ADD-INS panel: a command dialog sample, Show My Palette and Send to Palette.

Warning: If handlers are not stored in a persistent list, the command will appear to work once and then silently stop responding. This is the number one reason add-in buttons "do nothing".

Warning: Do not change the model inside inputChanged; Autodesk notes it clears selections. Do creation and edits in executePreview and execute, and keep the preview fast because it reruns after every input change.

Warning: Autodesk does not publish a full list of UI ids. Export them from your own installation with the "Write user interface to a file" sample before you hardcode them.

Practice exercise#

Build an add-in (use the Create dialog with type Add-In) that adds a button "Quick Box" to the ADD-INS panel (SolidScriptsAddinsPanel in the FusionSolidEnvironment workspace). The dialog should have a name text box and a distance input, and the execute handler should create a box of the given edge length using the sketch and extrude code from the earlier page. Expected result: the button appears when the add-in runs, the dialog opens with OK/Cancel, and the box is created on OK. Stopping the add-in removes the button.

  • Event handlers
  • Packaging and distributing add-ins
  • Debugging in VS Code
  • Creating sketches and features

Sources

Checked against these Autodesk help topics (opens autodesk.com):

Related pages