Driving the editor UI
UI driving lets an assistant read the Unreal Editor's own windows and widgets, and, only if you turn it on, click buttons, type into boxes and pick options. Read this if you want to know what it can reach, what it refuses, and how to switch it on or leave it off.
Off unless you turn it on
Reading the widget tree is always allowed. Acting on it is not. Clicking, typing, key presses, combobox selection and drags work only after a person ticks Allow Editor UI Driving under Project Settings, Plugins, Mif Bridge UI Driving. The setting is off by default. Even when it is on, the writes go through the safety gate and the call journal like any other write, and they are refused on controls that save, delete, quit, close the editor or discard work.
Your mouse is never moved. Where a widget has its own click function the bridge calls it directly. Otherwise it sends a pointer event to the widget's center inside the editor, and never repositions your cursor.
The seven endpoints
| Endpoint | Kind | What it does |
|---|---|---|
slate_snapshot |
read | The visible windows (or one subtree) as a tree of nodes: ref, type, role, label, state flags, geometry, tag, command. Takes ref, maxDepth, maxNodes, windowsOnly |
slate_find |
read | Nodes matching text (contains, or exact), type, role, tag, command or path, anywhere or within a ref. Each result carries its path |
slate_click |
write | Click a widget: left (the default), right, or doubleClick |
slate_type |
write | Type text into a text box. Optionally clear it first, and submit with Enter |
slate_press_key |
write | Press one unmodified navigation or editing key on a widget |
slate_select_option |
write | Open a combobox and pick an option by its text |
slate_drag |
write | Drag one widget (fromRef) onto another (toRef) |
The two reads work in every write mode. The five writes are refused in read mode and are subject to the setting above.
Other bridge calls already handled single cases: get_modal_dialog and answer_modal_dialog for dialogs, invoke_editor_command for bound commands, invoke_editor_tab for tabs and send_editor_key for a raw key. None of them could see or address an arbitrary widget. The slate_* endpoints can.
References
A ref looks like s12. It is assigned the first time a widget is reported and kept for that widget's whole life: the same widget always answers the same ref, across snapshots and finds, and the counter never resets. So a caller can snapshot once and act on refs later without reading again.
When the widget is destroyed, for example a menu closed or a panel rebuilt, its ref resolves to nothing and every endpoint refuses it by name with staleRef: true ("snapshot or find again"). A ref is never quietly re-pointed at a different widget with the same text, because a rebuilt list can put a different row under the same words.
Every node also carries a path: the window's title, then each meaningful ancestor's type and label, for example MyLevel > SDockTab "Content Browser" > SButton "Add". Use it for reading and for slate_find with path (a contains match). It is not an identity, because two buttons can share a path.
What is refused
A write is refused when any of these holds. The refusal names the rule (refusedRule), the widget, and the words or context that matched. Nothing is sent.
The control's own words
The bridge gathers the words on the target and on the control around it (the nearest button, check box, combo button, list row or menu entry): every text block inside it, its text tooltip, and for a menu or toolbar entry the bound command's name, label and description. Whole words match, ignoring case.
| Category | Words and command names |
|---|---|
| save | save, saved, unsaved, save all, save as, don't save. Any command name containing Save. The status bar's "All Saved" button counts, since it opens the Save Content dialog |
| delete | delete, force delete |
| quit or close | quit, exit, close, restart |
| discard | discard, revert, reload |
| play | play, simulate, launch. Play-In-Editor takes the editor loop, which is why start_pie is gated |
| cook or package | cook, package |
| source control | submit, check in |
| disk moves | move here, copy here, fix up, replace references, rename (in the Content Browser) |
Where the control is
| Location | Why |
|---|---|
| a tab header itself (not its content) | its only button is the tab's close button |
| a window's title bar | the window's own minimize, maximize and close |
| Project Settings and Editor Preferences | a change there is written to its .ini file at once, which is a save, and it is where this feature's own switch lives |
| the Plugins browser | enabling a plugin writes the project file |
| the MifBridge panel | it holds the write-mode control, and an assistant must not reach its own gate |
| the Content Browser's asset view and folder tree, for typing, Enter and drops | a rename or a drop into a folder there goes through the engine's rename and move, which save. That is why rename_asset and move_folder are gated |
Dialogs, keys and covered widgets
- Modal dialogs are not this feature's. While a modal dialog is up, the bridge refuses every call except the dialog's own.
slate_*cannot press a dialog's button.answer_modal_dialogdoes, under its own gate. - Keys.
slate_press_keytakes one key from a short list: Enter, Escape, Tab, BackSpace, the arrows, Home, End, PageUp, PageDown, SpaceBar. It takes no modifiers. The editor reads the real keyboard for Ctrl and Shift, so a chord such as Ctrl+S cannot be faked honestly, and Delete and the letter keys are not navigation. Enter and SpaceBar on a button are a click, so a key press is judged against the focused widget exactly as a click would be. - Covered widgets. A click lands on whatever is on top at that point, which need not be the widget named. Before a routed click is sent, the widget under that point is read. It must contain the target, and everything on that path is judged by the rules above. A covered target is refused as
occluded.
What this cannot catch
A control with harmless words whose code saves, such as a third-party panel's "Apply" that writes a file, cannot be recognized by its words. That is why the feature is off by default and the setting's tooltip says so. The scratch watch still reports a real asset that the click dirtied, and the protected-root check still refuses, after the fact, a click that changed engine content, as for any other write.
How a click is sent and checked
Where a widget has its own function, the bridge calls it, so nothing depends on the window being on screen, focused or painted:
| Widget | Route |
|---|---|
| button | its click function |
| check box | toggles its checked state, which fires the same event a click does |
| combo button or combo box | opens its menu |
| anything else, right-click, double-click, drags, a combobox option | a pointer event routed to the widget's center |
Typing focuses the box first and refuses unless the box then has focus, so characters never go to whatever else holds it. It sends one character event at a time, and an Enter after them when you pass submit.
Every write reads back in the same reply, in after:
- the target's state:
checked,text(for a text box it must equal what was typed, or the call answersok:false, verified:false),enabled, andexists(a button that closed its own panel is gone, and says so); - the windows that opened and closed (
windowsOpened[],windowsClosed[], by title), so a click that opened a menu or a dialog says so; - the keyboard focus afterward;
expectTextandexpectGone: optional text that must appear in, or disappear from, the visible tree after the action. When you give them,verifiedis their result and a miss isok:false.
delivered says the event was sent. verified says what was read back. They are never merged into one ok.
Dialogs raised by a click. The call runs under the bridge's unattended mode, so the engine's ordinary message boxes answer their default instead of showing. A custom modal window raised by third-party code would park the call until someone answers it. The reply of the next call names that hazard.
Asking first: dryRun
Each write takes dryRun: true. It resolves the ref, runs every refusal rule, and reports what would be sent without sending anything. It is the way to ask "would this be refused?". The dry run answers in every write mode, including read.
How the setting is protected
self_audit reports the switch as editorUIDriving: {allowed, settingInMemory, settingOnDisk, configFile, section, key}. A write is allowed only when both the setting in memory and the line in Config/DefaultEditor.ini on disk say true. Either alone is not enough, because each has a road an assistant could try: the settings object has a name another call could address, and the config file is something set_property with saveConfig can write in full mode.
On top of that, every bridge call that names the settings class or the property, in any string of its parameters at any depth, including inside a batch op, is refused in every write mode with refusedRule: "self-unlock". The Project Settings window is a refused context, so the switch cannot be clicked either. A refusal names the setting and where to tick it.
This mirrors the write mode's own rule. No endpoint, console variable or UI command can change the mode, so an assistant cannot unlock its own gate.
How it composes with other safety features
- Gate. The two reads pass in every mode. The five writes pass in
scratchandfulllike any write, are refused inreadmode, and get the scratch watch'sdirtiedRealPackagesreport and the protected-root check. They are not on the gate's by-name refusal list, because the refused-control rules and the setting do that job.send_editor_keyandinvoke_editor_commandstay on that list: they take a key or a command name with no target to judge. - Journal. Every call is journaled before it runs, so a click that kills the editor shows as a
startwith noend. The panel's Activity tab shows each one. - Undo. The writes are self-managed, like
invoke_editor_command: a click runs code the bridge did not write, which may open its own transaction or compile, so the bridge opens none around it. What a click did is undone the way you would undo it by hand. batch. Refused. Firing UI events insidebatch's single open transaction is the hazard above.- Change cap and previews. Not applicable. Each call is one action whose effect is unknown until it runs.
When a call is refused
| Failure | Answer |
|---|---|
| stale or unknown ref | refused with staleRef, nothing sent |
| widget disabled | refused with disabled |
| widget hidden, collapsed or zero size | refused for routed events (notOnScreen). The direct routes still work |
| target covered by another widget | refused with occluded, naming what is on top |
| text box never takes focus | refused with focusRefused, nothing typed |
| typed text does not read back | ok:false, verified:false, textAfter |
| combobox option not found | ok:false, the options that were found, and the dropdown is closed again |
| a modal dialog is up | refused by the modal guard before the handler runs |
| no Slate (a commandlet without a UI) | refused |