217 lines
9.5 KiB
Markdown
217 lines
9.5 KiB
Markdown
# 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\<id>\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<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`.
|