Skip to content

Add a button and a custom window

Build a useful plugin panel with an action, persistent state and the right closing behavior.

We will build a small panel: the user sets a maximum number of rows, clicks “Refresh list” and sees the names of the selected objects. The panel does not modify the project. It demonstrates how to keep state between openings and release event listeners when closed.

You should know how to install a minimal plugin. The example targets the current BDEngine Release and uses editorAPI and the standard DOM.

What the interface does

Download the complete window.js plugin. Install the file and restart the editor. Selection panel will appear in the dock menu.

“Maximum rows” accepts an integer from 1 to 50. This limit belongs to the example; it does not limit the number of objects in the project. An empty selection shows zero objects. Names are rendered as text: an object’s name is not executed as HTML.

The list refreshes when the window opens and when the button is clicked. Changing the selection while the panel is open does not refresh it automatically. The setting is kept in a plugin variable until the editor reloads, rather than saved in the project file.

Where to put the button

The dock menu works well for a panel that users open as needed:

editorAPI.addButtonDockMenu(
'icon-list',
'Selection panel',
openPanel
);

openPanel is a function defined by the plugin. The method returns an anchor DOM element. To remove this particular button later, keep the returned element and call its remove() method. Removing the button does not close a window that has already been created.

For a frequently used action, addButtonTool(icon, handler, right, sound) places a button in the toolbar. Import and export entries use separate methods. For parameters and return values, see choosing a button.

Register the button once after bde:started. Attach the event listener when the plugin file is executed:

window.addEventListener('bde:started', () => {
// Create the button and initialize the plugin state.
}, {once: true});

The editor interface exists at this point. This does not guarantee that the previous project has finished restoring: read the selection in response to a user action.

Regular or modal window

A regular window suits this panel: the user can change the selection and return to the list. createWindow creates and opens the window immediately:

const panel = editorAPI.createWindow(
'Selection panel',
'bde.docs.window.panel',
'icon-list',
460, 340,
300, 240,
cleanup
);

After the icon, pass the desired width and height, the minimum width and height, and the callback for closing with the window’s close icon. Sizes are in pixels. Use your own stable name_id: the editor uses it to associate saved window geometry with the window.

panel.container is the DOM container for the content. focus() brings an open window to the front and restores it if it was minimized.

createModalWindow(title) suits a temporary choice that blocks interaction with the editor. A modal window is created hidden: open it with showModal(), hide it with hideModal(), and add content to container. Hiding keeps the DOM and its event listeners. If you choose a modal, create it once and reuse it instead of creating another on every click.

Content and event listeners

The panel uses standard form, input, button and ul elements. The main part of the list update is:

const selected = editorAPI.getSelectedObjects();
list.replaceChildren();
for (const object of selected.slice(0, maxRows)) {
const row = document.createElement('li');
row.textContent = object.name || 'Unnamed object';
list.append(row);
}

getSelectedObjects() returns the current scene objects, not copies of their data. Here, we only read name. Before the call, validate the setting with Number.isInteger and range checks; input.min alone is not enough to validate values in code.

On form submission, call event.preventDefault() so the browser does not reload the page. Stop keydown and keyup events inside the form with stopPropagation() so typing in the input does not reach the editor’s keyboard handlers.

Attach the form’s listeners with a shared AbortController:

const events = new AbortController();
form.addEventListener('submit', handler, {signal: events.signal});
// When closing:
events.abort();

Do not insert values supplied by users into innerHTML. Use textContent for text and value for an input’s value.

Reopening and cleanup

The plugin keeps one reference to the window. While it exists, another click calls focus(). Closing the window removes the listeners and clears the reference.

There is an important difference between the two ways to close a regular window:

  • The window’s close icon calls the supplied closeFunc, then close().
  • Calling panel.close() directly does not call closeFunc by itself.

The panel’s own close button therefore performs both actions:

cleanup();
current.close();

close() removes the window DOM after its closing animation. A closed window is not reopened: the next click creates a new one. The maxRows setting lives outside the window, so it survives between openings.

If the tool later uses setInterval, listeners on window or other external resources, release those in your own cleanup too. Removing the DOM does not do this automatically. closeFunc is not a hook for unloading the entire plugin. See checking the lifecycle.

Verify behavior and state

  1. Open the panel with nothing selected. It should show zero objects and an empty list.
  2. Select several objects and click “Refresh list”. Check their names and the count.
  3. Set the maximum to 1. The list should contain one row without changing the total selection count.
  4. Try an empty field, 0, 51 and a fractional number. The invalid value must not be applied.
  5. While the window is open, click its dock entry again. The same window should become active.
  6. Close it with the window’s close icon, then reopen it. Repeat using the panel’s own “Close” button. In both cases, the setting should be retained and one form submission should cause one update.

These are checks to perform in the editor. Reviewing the source and checking JavaScript do not replace testing focus, minimization and window sizes on the intended device.