# 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](api-reference.md); copy-paste recipes are in [examples.md](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** * `CreateView` must 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 a `Window`, and do not add your element to another parent. * Position with `HorizontalAlignment`/`VerticalAlignment`/`Margin`, or use a `Canvas` and `Canvas.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 with `InteractiveMode`. * Transparent pixels cost nothing visually; opaque full-screen rectangles do. Prefer text, `Path`, `Ellipse`, gradients with alpha. * Avoid `BitmapEffect`/heavy `Effect` on large elements and avoid animating layout properties; prefer `RenderTransform` animations. * Timers: `DispatcherTimer` with `DispatcherPriority.Background`, interval ≥ 1 s for clocks, ~16 ms for animation, and stop them in `OnVisibilityChanged(false)`. Remember `Update` is 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. ```csharp 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** * `View` must be a single element you own; LiveWall puts it in the wallpaper window. Keep the background opaque (or it shows the desktop colour behind). * `Load` may 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 after `Load`, and repeatedly; make it idempotent. * Raise `Failed(message)` for problems after load (device lost, decode error) and `Ended` when a non-looping source finished, so LiveWall can advance the playlist. * Reported `WallpaperState.RendererName` shows in diagnostics — return a stable `Name`. * 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: ```csharp 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 `TrayMenu` permission it also appears in the tray under **Plugins ▸**; the submenu is rebuilt whenever plugins load/unload. * `CanExecute` is evaluated when the menu opens and in the row rendering — keep it cheap and side-effect free. * The `ctx` gives you `ctx.Plugin` (your context) and `ctx.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\\storage.json`. ```csharp 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](api-reference.md#events-eventscs) for the catalogue. Two habits: ```csharp // keep the subscription, dispose it in Shutdown _subs.Add(context.Events.Subscribe(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`.