UI extensions (QML)
Replace the host's UI components with QML (backplate, VU meter, caption view, decorations…). For the slot list and the background-drawing guidelines see UI slots — this page only covers how to write it.
Declaration
Use theme as the type (for a partial slot extension, style_layer: "effects" is recommended):
{
"id": "community.theme.neon_wave",
"name": "极光霓虹律动波形条",
"version": "1.0.0",
"target_app_version": "0.2",
"type": "theme",
"style_layer": "effects",
"ui_extensions": {
"audio_level": "CustomWaveform.qml"
},
"enabled": false
}Only use slots the host already consumes; overlay_root / style_bar currently have no Loader — do not use them.
Injected properties
| Property | Applies to | Description |
|---|---|---|
hostContext | Overlay-window components | style / subtitle / audio / window / invoke |
bridge | All | Public slots (aligned with Host SPI), e.g. bridge.getPluginConfig(pluginId) |
pluginId | All | Your plugin id |
appState | Background components | Global app state (bgColor / bgOpacity / neonBorderColor…) |
window / targetWindow | Background components | Pointer to the target window instance |
customRadius | Background components | Corner radius of the target window, injected by the host (Settings window 16, History window 16, generic dialogs 12…) |
hovered | Overlay window | Whether the mouse is currently hovering |
A property is assigned only if your component declares it — don't declare the ones you don't need.
For the complete property list and HostContext, see QML UI slots & HostContext.
Reading config: you must use Connections
⚠️
bridge.getPluginConfig(pluginId)is a transient function call, not a QML reactive property. If you read it only once, your component will not update when the user flips a switch in the settings panel.
import QtQuick 2.15
Item {
id: root
anchors.fill: parent
readonly property string myPluginId: "community.theme.my_theme"
// 1. Initial read
property var cfg: (typeof bridge !== "undefined" && bridge && bridge.getPluginConfig)
? bridge.getPluginConfig(root.myPluginId) : ({})
// 2. Unwrap + defaults
readonly property bool enableGlow: (cfg && typeof cfg.neon_glow !== "undefined") ? cfg.neon_glow : true
// 3. You must listen to the signal and refresh cfg on the fly
Connections {
target: (typeof bridge !== "undefined") ? bridge : null
function onPluginConfigChanged(pid) {
if (pid === root.myPluginId) {
root.cfg = bridge.getPluginConfig(root.myPluginId) || ({})
}
}
}
// 4. Bind to the visual layer
Rectangle {
anchors.fill: parent
visible: root.enableGlow
border.color: "#00f0ff"
border.width: 1.5
}
}Visual rules (hard requirements)
- Every plugin QML must adapt to theme switching automatically through
ThemeTokensandPluggableWindowBackground. - Emojis or text characters must not be used as icons; use vector SVG (
ThemeTokens.iconor an SVG shipped with the plugin). - Do not hard-code font families / icon colors inside components — hard-coded values give you away the moment the user changes theme; use
ThemeTokens.*. - Background-type slots must draw their own border and adapt their corner radius; for details see UI slots § Background slot guidelines.
Calling the plugin's Python actions
On the QML side, call the plugin's action_* through the slot:
// Synchronous (blocks the UI thread; only suitable for instant actions)
bridge.invokePluginAction(pluginId, "run", {"source": "ui"})
// For long-running actions use the async version, or have the Python side go through self.run_in_background()
bridge.invokePluginActionAsync(pluginId, "ask", {"prompt": "…"})Events raised by the plugin are received in QML by pluginEvent(plugin_id, event_name, payload); see Event bus.
Hot debugging
After editing QML: disable and re-enable the plugin to reload it; or restart the app (during development you can use the host's hot-reload capability with TRANSBOX_QML_RELOAD=1, see Hot debugging).
Related
- UI slots — slot list + the three background-slot guidelines
- theme — tokens and caption styles
- Event bus
- Example library
