using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Effects; using System.Windows.Threading; using LiveWall.Sdk; namespace ClockOverlayPlugin; /// /// Example plugin that touches every extension point, in the order a real plugin would use them: /// /// 1. a desktop overlay (a clock, drawn above the wallpaper), /// 2. a settings page (cursor colour, 12/24 h, show wallpaper name), /// 3. a command ("Say hello") that appears in the app and the tray menu, /// 4. a wallpaper filter that can veto changes while "pin wallpaper" is on, /// 5. a global hotkey (Ctrl+Alt+F9), /// 6. storage (colour + flags survive restarts), /// 7. event subscriptions (wallpaper changes, playback changes). /// public sealed class ClockPlugin : ILiveWallPlugin { private IPluginContext? _context; private IDisposable? _wallpaperSub; private IDisposable? _playbackSub; private string _colour = "#FFFFFF"; private bool _use24Hour = true; private bool _showWallpaperName = true; private bool _pinWallpaper; public void Initialize(IPluginContext context) { _context = context; var log = context.Log; log.Info($"Clock plugin starting (plugin API {PluginApi.Version}, host {PluginApi.HostVersion})"); // ---- 6. persisted settings ------------------------------------------------- _colour = context.Storage.Get("colour", "#FFFFFF") ?? "#FFFFFF"; _use24Hour = context.Storage.Get("use24Hour", true); _showWallpaperName = context.Storage.Get("showWallpaperName", true); _pinWallpaper = context.Storage.Get("pinWallpaper", false); // ---- 1. overlay ------------------------------------------------------------ context.Extensions.AddOverlay(new ClockOverlay(this)); // ---- 2. settings page ------------------------------------------------------ context.Extensions.AddSettingsPage(new ClockSettingsPage(this, context)); // ---- 3. command ------------------------------------------------------------ context.Extensions.AddCommand(new PluginCommand("clock.hello", "Say hello from the clock plugin", ctx => { ctx.Host.ShowNotification("Clock plugin", "Hello! Your wallpaper is " + Describe(ctx.Host.Current)); ctx.Host.SetStatus("Clock plugin says hello"); }) { Category = "Example", CanExecute = () => _context != null }); // ---- 4. wallpaper filter --------------------------------------------------- context.Extensions.AddFilter(new PinWallpaperFilter(this)); // ---- 5. hotkey ------------------------------------------------------------- context.Extensions.AddHotkey(new PluginHotkey("clock.toggle24", "Ctrl+Alt+F9", () => { _use24Hour = !_use24Hour; Save(); _context?.Host.SetStatus(_use24Hour ? "Clock: 24 hour" : "Clock: 12 hour"); }) { Description = "Clock: toggle 12/24 hour" }); // ---- 7. events ------------------------------------------------------------- _wallpaperSub = context.Events.Subscribe(e => context.Log.Info($"Wallpaper changed: {e.Kind} {e.Source} via {e.RendererName}")); _playbackSub = context.Events.Subscribe(e => context.Log.Debug($"Playback: playing={e.IsPlaying} paused={e.IsPaused}")); context.Events.Subscribe(e => context.Log.Debug($"Interactive mode: {e.Interactive}")); } public void Shutdown() { _wallpaperSub?.Dispose(); _playbackSub?.Dispose(); _context = null; } // ------------------------------------------------------------------ used by the overlay / page public string Colour => _colour; public bool Use24Hour => _use24Hour; public bool ShowWallpaperName => _showWallpaperName; public bool PinWallpaper => _pinWallpaper; public void SetColour(string colour) { _colour = colour; Save(); Refresh(); } public void SetUse24Hour(bool value) { _use24Hour = value; Save(); Refresh(); } public void SetShowWallpaperName(bool value) { _showWallpaperName = value; Save(); Refresh(); } public void SetPinWallpaper(bool value) { _pinWallpaper = value; Save(); } private void Save() { var storage = _context?.Storage; if (storage == null) return; storage.Set("colour", _colour); storage.Set("use24Hour", _use24Hour); storage.Set("showWallpaperName", _showWallpaperName); storage.Set("pinWallpaper", _pinWallpaper); } private void Refresh() => _overlay?.Refresh(); private ClockOverlay? _overlay; private void AttachOverlay(ClockOverlay overlay) => _overlay = overlay; private static string Describe(WallpaperState state) => string.IsNullOrEmpty(state.Kind) ? "(unknown)" : $"{state.Kind}: {state.Source}"; // ------------------------------------------------------------------ the overlay itself private sealed class ClockOverlay : IWallpaperOverlay { private readonly ClockPlugin _plugin; private TextBlock? _clock; private TextBlock? _source; private DispatcherTimer? _timer; public ClockOverlay(ClockPlugin plugin) { _plugin = plugin; plugin.AttachOverlay(this); } public string Name => "Clock"; public FrameworkElement CreateView(OverlayContext context) { var panel = new StackPanel { HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0, 24, 32, 0), IsHitTestVisible = false }; _clock = new TextBlock { FontFamily = new FontFamily("Segoe UI Light"), FontSize = 46, Foreground = ParseBrush(_plugin.Colour), TextAlignment = TextAlignment.Right, Effect = new DropShadowEffect { BlurRadius = 18, ShadowDepth = 0, Opacity = 0.85, Color = Colors.Black } }; _source = new TextBlock { FontSize = 12, Foreground = new SolidColorBrush(Color.FromArgb(180, 255, 255, 255)), TextAlignment = TextAlignment.Right, Margin = new Thickness(0, 2, 2, 0), Visibility = _plugin.ShowWallpaperName ? Visibility.Visible : Visibility.Collapsed }; panel.Children.Add(_clock); panel.Children.Add(_source); _timer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromSeconds(1) }; _timer.Tick += (_, _) => Tick(); _timer.Start(); Tick(); return panel; } public void Update(OverlayState state) { if (_source != null && _plugin.ShowWallpaperName) { _source.Text = string.IsNullOrEmpty(state.Kind) ? "" : $"{state.Kind} ยท {System.IO.Path.GetFileName(state.Source)}"; _source.Visibility = Visibility.Visible; } else if (_source != null) { _source.Visibility = Visibility.Collapsed; } } public void OnVisibilityChanged(bool visible) { if (visible) _timer?.Start(); else _timer?.Stop(); } public void Refresh() { if (_source != null) _source.Visibility = _plugin.ShowWallpaperName ? Visibility.Visible : Visibility.Collapsed; Tick(); } private void Tick() { if (_clock == null) return; var now = DateTime.Now; string format = _plugin.Use24Hour ? "HH:mm" : "h:mm tt"; _clock.Text = now.ToString(format, CultureInfo.CurrentCulture); _clock.Foreground = ParseBrush(_plugin.Colour); } private static Brush ParseBrush(string hex) { try { return (Brush)new BrushConverter().ConvertFromString(hex)!; } catch { return Brushes.White; } } } // ------------------------------------------------------------------ the filter private sealed class PinWallpaperFilter : IWallpaperFilter { private readonly ClockPlugin _plugin; public PinWallpaperFilter(ClockPlugin plugin) => _plugin = plugin; public int Priority => 0; public WallpaperRequest Apply(WallpaperRequest request, IHostActions host) { if (!_plugin.PinWallpaper) return request; // "Pin wallpaper" means: keep whatever is on screen, refuse the change. host.SetStatus("Clock plugin: wallpaper changes are pinned"); return new WallpaperRequest { Kind = host.Current.Kind, Source = host.Current.Source, Title = "pinned" }; } } // ------------------------------------------------------------------ the settings page private sealed class ClockSettingsPage : IPluginSettingsPage { private readonly ClockPlugin _plugin; private readonly IPluginContext _context; public ClockSettingsPage(ClockPlugin plugin, IPluginContext context) { _plugin = plugin; _context = context; } public string Title => "Clock"; public string Description => "Position, colour and format of the clock overlay."; public FrameworkElement CreateView() { var panel = new StackPanel(); panel.Children.Add(new TextBlock { Text = "Colour (any WPF colour syntax)", Margin = new Thickness(0, 0, 0, 4), Foreground = new SolidColorBrush(Color.FromRgb(0x8C, 0x95, 0xA5)) }); var colourBox = new TextBox { Text = _plugin.Colour, Width = 160, HorizontalAlignment = HorizontalAlignment.Left }; colourBox.TextChanged += (_, _) => _plugin.SetColour(colourBox.Text); panel.Children.Add(colourBox); var use24 = new CheckBox { Content = "24 hour clock", IsChecked = _plugin.Use24Hour, Margin = new Thickness(0, 12, 0, 0) }; use24.Checked += (_, _) => _plugin.SetUse24Hour(true); use24.Unchecked += (_, _) => _plugin.SetUse24Hour(false); panel.Children.Add(use24); var showName = new CheckBox { Content = "Show the wallpaper name under the clock", IsChecked = _plugin.ShowWallpaperName, Margin = new Thickness(0, 8, 0, 0) }; showName.Checked += (_, _) => _plugin.SetShowWallpaperName(true); showName.Unchecked += (_, _) => _plugin.SetShowWallpaperName(false); panel.Children.Add(showName); var pin = new CheckBox { Content = "Pin the wallpaper (this plugin's filter blocks changes)", IsChecked = _plugin.PinWallpaper, Margin = new Thickness(0, 8, 0, 0) }; pin.Checked += (_, _) => _plugin.SetPinWallpaper(true); pin.Unchecked += (_, _) => _plugin.SetPinWallpaper(false); panel.Children.Add(pin); panel.Children.Add(new TextBlock { Text = "Storage file: " + _context.Storage.PathFor("storage.json"), Margin = new Thickness(0, 16, 0, 0), FontSize = 11, TextWrapping = TextWrapping.Wrap, Foreground = new SolidColorBrush(Color.FromRgb(0x8C, 0x95, 0xA5)) }); return panel; } } }