9.5 KiB
Extension points in detail
This page is about behaviour: what each extension point is really good for, how the host drives it, and the caveats that only show up once you run it. Signatures are in api-reference.md; copy-paste recipes are in examples.md.
Registration order does not matter — everything registered inside Initialize is collected and then
applied once the plugin finishes initialising.
1. Overlays — drawing on the desktop
context.Extensions.AddOverlay(new MyOverlay()) (permission DesktopOverlay).
Where it draws. LiveWall owns one transparent, layered, always-behind window per display inside
the shell's desktop layer (the same WorkerW the wallpaper lives in). Above the wallpaper and below
the icons, it hosts a Canvas — DesktopIconWindow.OverlayLayer — and each plugin's view is
stretched across it, in registration order (so a later plugin paints over an earlier one).
Lifecycle
| Host action | Your callbacks |
|---|---|
| Plugin enabled / rescan | CreateView(context) once per display |
| Wallpaper or playback changed | Update(state) |
| Overlays hidden (tray Hide overlay), plugin disabled, wallpaper paused | OnVisibilityChanged(false) |
| Overlays shown again | OnVisibilityChanged(true) |
| Plugin disabled | your view is removed and collected |
Rules that matter
CreateViewmust return a fresh element each time it is called (it is called once per display, and again if the overlay host is rebuilt). Do not return aWindow, and do not add your element to another parent.- Position with
HorizontalAlignment/VerticalAlignment/Margin, or use aCanvasandCanvas.SetLeft/Top. Do not assume a specific size: the root is stretched to the display, in DIPs. - No hit testing. The layer is
IsHitTestVisible = false, so mouse events never reach you and never block the desktop. If you need clicks, use a renderer withInteractiveMode. - Transparent pixels cost nothing visually; opaque full-screen rectangles do. Prefer text,
Path,Ellipse, gradients with alpha. - Avoid
BitmapEffect/heavyEffecton large elements and avoid animating layout properties; preferRenderTransformanimations. - Timers:
DispatcherTimerwithDispatcherPriority.Background, interval ≥ 1 s for clocks, ~16 ms for animation, and stop them inOnVisibilityChanged(false). RememberUpdateis not a timer. - Don't paint over the icon area with anything opaque — the whole point of the layer is that icons stay usable.
Multi-monitor. One CreateView per display, each with its own OverlayContext; use
context.Primary if you only want the main screen (return an empty Canvas for the others).
2. Wallpaper filters — rewriting requests
context.Extensions.AddFilter(new MyFilter()) (permission Filters).
The engine builds a WallpaperRequest (Kind, Source, Title, Metadata) and asks every filter,
highest Priority first, to transform it. What you return is applied — including the built-in
renderers' input, playlist advance, and requests made by other plugins via
host.SetWallpaper.
Good uses:
- resolve a shorthand (
yt:meatloaf→ a real URL), - expand environment variables (
%USERPROFILE%\pics\a.mp4), - pick a random file from a folder when the source is
random:folder, - add metadata for your own renderer (
request.Metadata["scene"] = "rain"), - veto: return the current wallpaper (
host.Current) to refuse the change, - normalise casing/slashes so two plugins agree on a path.
Bad uses:
- doing I/O on the UI thread (filters run synchronously in the wallpaper path — a 3-second network call looks like a frozen app),
- returning
null(treated as "no change", but do it explicitly by returning the argument), - mutating the request object you were given and returning a different one.
Filter exceptions are caught per filter per pass; the pass continues with the previous request.
3. Renderers — playing what nobody else can
context.Extensions.AddRenderer(new MyRenderer()) (permission Renderer).
CanRender(request) is asked for every request, in descending Priority (ties run in plugin load
order). The first true wins and the built-in players are skipped entirely for that source, so you can
also replace video playback with your own pipeline.
public bool CanRender(WallpaperRequest r)
=> r.Kind.Equals("Custom", StringComparison.OrdinalIgnoreCase) && r.Source.StartsWith("myprefix:");
Then Create() → Load(request) → (Play/Pause/ApplyPlayback as the user acts) → Unload() →
Dispose().
Rules
Viewmust be a single element you own; LiveWall puts it in the wallpaper window. Keep the background opaque (or it shows the desktop colour behind).Loadmay throw — that is how you report "this file is not what I expected"; the message goes to the status line and the log.ApplyPlayback(muted, volume /*0–100*/, speed /*0.25–4*/)can arrive before or afterLoad, and repeatedly; make it idempotent.- Raise
Failed(message)for problems after load (device lost, decode error) andEndedwhen a non-looping source finished, so LiveWall can advance the playlist. - Reported
WallpaperState.RendererNameshows in diagnostics — return a stableName. - Do not start a second renderer yourself; LiveWall creates one and keeps it for the source's lifetime.
Interaction. With Displays.Interaction = Interactive the wallpaper window accepts clicks, so a
renderer can host real UI (a game, a web view, an editor). In the default ClickThrough mode the
window is transparent to the mouse and the desktop icons above it stay clickable.
4. Settings pages
context.Extensions.AddSettingsPage(new MyPage()) (permission SettingsUi).
The page shows up in Plugins on the plugin's row via Settings. One page → one window; several
pages → one window with tabs. CreateView runs when the window opens, so read your storage there and
build fresh controls each time (do not cache the element).
LiveWall's resource dictionary is available through Application.Current.Resources, so plugin pages
can look native:
var label = new TextBlock
{
Text = "Colour",
Foreground = (Brush)Application.Current.Resources["TextDim"]
};
Keep it declarative: you get a ScrollViewer around your content, so a long StackPanel is fine.
5. Commands and the tray menu
context.Extensions.AddCommand(new PluginCommand("id", "Title", ctx => …)).
- Every command is listed in Plugins ▸ Plugin commands with a Run button.
- With the
TrayMenupermission it also appears in the tray under Plugins ▸; the submenu is rebuilt whenever plugins load/unload. CanExecuteis evaluated when the menu opens and in the row rendering — keep it cheap and side-effect free.- The
ctxgives youctx.Plugin(your context) andctx.Host(actions). - Exceptions inside a command are caught, logged, and reported in the status line.
Use commands for anything the user should be able to trigger: "Next quote", "Toggle rain", "Open my plugin's data folder", "Reload my config".
6. Hotkeys
context.Extensions.AddHotkey(new PluginHotkey("id", "Ctrl+Alt+K", () => …)) (permission Hotkeys).
Gestures use LiveWall's own parser: one or more modifiers (Ctrl, Alt, Shift, Win), then one
key — F1–F24, A–Z, 0–9, D0–D9, Space, Pause, PrintScreen, Insert, Delete,
Home, End, PageUp, PageDown, arrows, NumPad0–NumPad9, NumPad*. LiveWall's own shortcuts
are registered first; a collision logs a warning and the plugin hotkey is skipped. Hotkeys are
unregistered when the plugin unloads.
The action runs on the UI thread — nothing long inside it.
7. Storage
context.Storage is a JSON object per plugin in plugin-data\<id>\storage.json.
var colour = context.Storage.Get("colour", "#FFFFFF");
context.Storage.Set("colour", "#00FF88"); // debounced write
File.WriteAllText(context.Storage.PathFor("cache.json"), json); // your own files too
Use PathFor for anything larger than a few values — everything in the key/value store is rewritten
as one small document. Flush() forces a write, and LiveWall flushes automatically on shutdown.
8. Events and host actions
See api-reference.md for the catalogue. Two habits:
// keep the subscription, dispose it in Shutdown
_subs.Add(context.Events.Subscribe<WallpaperChangedEvent>(OnWallpaperChanged));
public void Shutdown() { foreach (var s in _subs) s.Dispose(); _subs.Clear(); }
and never block the UI thread from a handler — they run on it.
IHostActions is the write side: SetWallpaper, Play, Pause, TogglePause, NextWallpaper,
ReloadWallpaper, Reattach, OpenSettings, SetStatus, ShowNotification, SetOverlayVisible.
Each is a no-op (with a log line) if you did not declare the matching permission.
SetWallpaper goes back through the filter chain, so two plugins that both set wallpapers cannot
deadlock each other — the last write wins and every filter gets a say.
Putting it together: what a "big" plugin looks like
The sample in samples/ClockOverlayPlugin/ uses all of the above at once and is the best reference:
- overlay + settings page + command + filter + hotkey + storage + three event subscriptions,
- the settings page writes storage and the overlay picks the change up immediately,
- the filter pins the wallpaper while its option is on,
- the command reports the current wallpaper through
ctx.Host.Current.