Skip to content

FormBuilder.md

eManF edited this page Aug 26, 2026 · 1 revision

FormBuilder

FormBuilder is Macro Studio's schema-driven dialog system. Commands describe the inputs they need as Python dictionaries, and FormBuilder turns those descriptions into consistent, theme-aware eDock forms.

This keeps command implementations focused on automation logic instead of repeated dialog code.

How it works

When a user adds or edits a command with fields, Macro Studio follows this path:

Command.fields
      ↓
FormBuilder.get_data(...)
      ↓
The field registry creates widgets
      ↓
The user edits and validates values
      ↓
A values dictionary is returned to the command

A command with no fields is added immediately. A command with fields opens a FormBuilder dialog first. The same builder is also used by project and variable dialogs.

Schema basics

A schema contains a title, optional submit button text, and a list of fields:

schema = {
    "title": "Type a greeting",
    "submit_text": "Add",
    "fields": [
        {
            "name": "text",
            "title": "Message",
            "value_type": "string",
            "default_value": "Hello from Macro Studio",
            "required": True,
        },
    ],
}

Common field properties:

Property Purpose
name Key used in the returned values dictionary
title Label shown beside the widget
value_type Widget and value format to create
default_value Initial value
required Whether the user must provide a value
place_holder Hint shown when a text field is empty
options Choices for choice, radio, and checkbox fields
visible_if Show the field only when one rule matches
visible_if_all Show the field only when every rule matches
visible_if_any Show the field when at least one rule matches
enabled_if Keep the field visible but disable it unless the rule matches
hidden Remove the field from the form
os Restrict the field to an operating system

Unknown or omitted value_type values fall back to a string field.

Opening a form

Use FormBuilder.get_data when a command or dialog needs user input:

from ...ui.forms.form_builder import FormBuilder

data = FormBuilder.get_data(
    schema={
        "title": "Create a note",
        "submit_text": "Create",
        "fields": [
            {
                "name": "title",
                "title": "Title",
                "value_type": "string",
                "required": True,
            },
            {
                "name": "body",
                "title": "Body",
                "value_type": "textarea",
                "place_holder": "Write your note...",
            },
        ],
    },
    values={"title": "", "body": ""},
    parent=window,
)

if data is None:
    return  # The user pressed Cancel.

title = data["title"]
body = data["body"]

get_data returns a dictionary after Save/Add, and None when the dialog is cancelled. Values are normalized by the field handler, so integers, booleans, ranges, colors, paths, and structured fields arrive in their expected form.

Built-in field types

The default registry includes:

string, text, textarea, code, python_code, integer, int, float, double, min_max, range, boolean, bool, choice, radio_group, checkbox_group, color, mouse_position, file, folder, captured_image, screen_region, audio, variable, comment, macro_group, status, result, and message.

Aliases such as text_area, multiline, directory, and position are supported for readability in command schemas.

Example: conditional fields

A common pattern is to show a position editor only when the user chooses positional clicking:

fields = [
    {
        "name": "click_mode",
        "title": "Click Mode",
        "value_type": "choice",
        "options": ["current", "position"],
        "default_value": "current",
    },
    {
        "name": "position",
        "title": "Position",
        "value_type": "mouse_position",
        "default_value": {"x": 0, "y": 0},
        "visible_if": {
            "field": "click_mode",
            "operator": "==",
            "value": "position",
        },
    },
]

When click_mode is current, the position field is hidden. When it changes to position, FormBuilder reevaluates the rule immediately and resizes the dialog.

For multiple dependencies, use visible_if_all or visible_if_any:

{
    "name": "x_variable",
    "title": "X Variable",
    "value_type": "variable",
    "visible_if_all": [
        {"field": "click_mode", "operator": "==", "value": "position"},
        {"field": "position_type", "operator": "==", "value": "variable"},
    ],
}

Supported comparison operators include ==, !=, in, not in, empty, not empty, is true, and is false.

Example: choices and validation

{
    "name": "format",
    "title": "Output format",
    "value_type": "choice",
    "options": ["plain_text", "markdown", "json"],
    "default_value": "plain_text",
    "required": True,
}

Required fields are checked on submit. Empty visible fields are highlighted and the dialog stays open until they are fixed.

For comments and jump targets, FormBuilder can validate that a selected comment exists:

{
    "name": "target",
    "title": "Jump to comment",
    "value_type": "comment",
    "required": True,
}

Macro Studio supplies the current runtime comments and variables to the form, so these fields can offer live project choices.

Runtime variables and comments

When Macro Studio opens a command form, it passes project context:

data = FormBuilder.get_data(
    schema={"title": "Command", "fields": command.fields},
    values=command.default_values(),
    parent=window,
    runtime_variables=window.collect_defined_variables(),
    runtime_comments=controller.collect_defined_comments(),
)

This enables:

  • value_type: "variable" fields to list existing variables
  • adding a new variable from a variable selector
  • value_type: "comment" fields to list comment markers
  • value_type: "macro_group" fields to list project macro groups

Computed and status fields

A field can derive a preview from the current form values with compute_value:

{
    "name": "preview",
    "title": "Preview",
    "value_type": "status",
    "required": False,
    "compute_value": lambda values: {
        "value": f"{values.get('prefix', '')}{values.get('name', '')}",
        "status": "info",
    },
}

Computed fields refresh whenever another field changes. status, result, and message fields are informational and are not required by default.

Extending the registry

For a project-specific widget, implement a field handler following the existing handlers in ui/forms/fields/, then register it:

from ...ui.forms.form_builder import FormBuilder

FormBuilder.register_field("my_type", MyFieldHandler(context))

The handler is responsible for creating the widget, reading its value, writing a value, and connecting its change signal. Prefer adding a reusable handler when the field is useful to more than one command.

Practical guidance

  • Keep schemas declarative and let execute() consume the returned values.
  • Give every field a stable name; it becomes part of saved macro data.
  • Use default_value so a new command is usable immediately.
  • Use conditional visibility to avoid irrelevant options.
  • Mark informational fields as required: False.
  • Use variable, comment, and macro_group types instead of duplicating project-selection widgets.
  • Keep callbacks side-effect-free; use them to calculate display state or update form context.

Clone this wiki locally