diff --git a/src/DesktopMagic/BuiltInPlugins/WeatherPlugin.cs b/src/DesktopMagic/BuiltInPlugins/WeatherPlugin.cs index 722cd2c..929e87f 100644 --- a/src/DesktopMagic/BuiltInPlugins/WeatherPlugin.cs +++ b/src/DesktopMagic/BuiltInPlugins/WeatherPlugin.cs @@ -58,9 +58,6 @@ public class WeatherPlugin : AsyncPlugin showTime.OnValueChanged += Application.UpdateWindow; fontSizeSlider.OnValueChanged += Application.UpdateWindow; - await UpdateLocationAndWeather(); - Application.UpdateWindow(); - searchButton.OnClick += () => { isLoading = true; @@ -73,6 +70,9 @@ public class WeatherPlugin : AsyncPlugin Application.UpdateWindow(); }); }; + + await UpdateLocationAndWeather(); + Application.UpdateWindow(); } public override async Task MainAsync(CancellationToken cancellationToken) diff --git a/src/DesktopMagic/Manager.cs b/src/DesktopMagic/Manager.cs index 350a8c1..70c6267 100644 --- a/src/DesktopMagic/Manager.cs +++ b/src/DesktopMagic/Manager.cs @@ -291,6 +291,32 @@ public sealed class Manager SaveSettings(); } + private readonly Dictionary<(Layout, uint), SettingSynchronizer> settingSynchronizers = []; + + /// + /// Gets (or creates) the synchronizer that keeps the settings of all windows showing the + /// given plugin in the given layout in sync. + /// + internal SettingSynchronizer GetSettingSynchronizer(Layout layout, uint pluginId) + { + (Layout, uint) key = (layout, pluginId); + if (!settingSynchronizers.TryGetValue(key, out SettingSynchronizer? synchronizer)) + { + synchronizer = new SettingSynchronizer(); + settingSynchronizers.Add(key, synchronizer); + } + + return synchronizer; + } + + /// + /// Drops the synchronizer for the given plugin in the given layout once no windows use it anymore. + /// + internal void ReleaseSettingSynchronizer(Layout layout, uint pluginId) + { + _ = settingSynchronizers.Remove((layout, pluginId)); + } + #endregion #region Settings Management diff --git a/src/DesktopMagic/Plugins/IPluginWindow.cs b/src/DesktopMagic/Plugins/IPluginWindow.cs index 1dce738..d840b7f 100644 --- a/src/DesktopMagic/Plugins/IPluginWindow.cs +++ b/src/DesktopMagic/Plugins/IPluginWindow.cs @@ -21,5 +21,8 @@ public interface IPluginWindow void Hide(); void Close(); + void ApplySettingValue(string id, string value); + void ApplyButtonClick(string id); + event System.ComponentModel.CancelEventHandler Closing; } diff --git a/src/DesktopMagic/Plugins/PluginWindow.xaml.cs b/src/DesktopMagic/Plugins/PluginWindow.xaml.cs index 531cfea..bcc0205 100644 --- a/src/DesktopMagic/Plugins/PluginWindow.xaml.cs +++ b/src/DesktopMagic/Plugins/PluginWindow.xaml.cs @@ -9,6 +9,7 @@ using DesktopMagic.Settings; using SkiaSharp; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; @@ -44,6 +45,7 @@ public partial class PluginWindow : Window, IPluginWindow private readonly Rectangle screenBounds; private readonly string screenDeviceName; private bool isUpdatingPosition = false; + private bool _editMode = false; private CancellationTokenSource? pluginCancellationTokenSource; private FileSystemWatcher? pluginFileWatcher; @@ -58,8 +60,13 @@ public partial class PluginWindow : Window, IPluginWindow private readonly PropertyChangedEventHandler themePropertyChangedHandler; private Theme? subscribedTheme; private NotifyCollectionChangedEventHandler? themesCollectionChangedHandler; - private readonly List subscribedSettings = []; + private readonly List<(Setting Setting, Action Handler)> subscribedSettings = []; private readonly List<(Setting Setting, Action Handler)> defaultSettingsSubscriptions = []; + private readonly List<(Button Button, Action Handler)> subscribedButtonClicks = []; + + private readonly ConcurrentDictionary localSettings = []; + private SettingSynchronizer? synchronizer; + private bool suppressButtonSync = false; public bool IsRunning { get; private set; } = true; public PluginMetadata PluginMetadata { get; private set; } @@ -121,6 +128,12 @@ public partial class PluginWindow : Window, IPluginWindow PluginFolderPath = pluginFolderPath; + if (settings.Owner is not null) + { + synchronizer = Manager.Instance.GetSettingSynchronizer(settings.Owner, PluginMetadata.Id); + synchronizer.Register(this); + } + assemblyLoadContext = CreateAssemblyLoadContext(); // Initialize hot reload watcher if plugin supports unloading and is external @@ -346,6 +359,8 @@ public partial class PluginWindow : Window, IPluginWindow public void SetEditMode(bool enabled) { + _editMode = enabled; + if (enabled) { Topmost = true; @@ -646,7 +661,14 @@ public partial class PluginWindow : Window, IPluginWindow void SetWindowLayer() { - WindowPos.SetWindowLayer(this, pluginClassInstance.windowLayer.Value); + if (_editMode) + { + Topmost = true; + } + else + { + WindowPos.SetWindowLayer(this, pluginClassInstance.windowLayer.Value); + } } void SetRotation() @@ -686,7 +708,7 @@ public partial class PluginWindow : Window, IPluginWindow defaultSettingsSubscriptions.Add((setting, handler)); } - private void OnPluginSettingValueChanged() + private void OnPluginSettingValueChanged(Setting setting, string id) { pluginClassInstance?.OnSettingsChanged(); @@ -694,6 +716,34 @@ public partial class PluginWindow : Window, IPluginWindow { pluginClassInstance.Application.UpdateWindow(); } + + synchronizer?.SettingChanged(this, id, setting.GetJsonValue()); + } + + public void ApplySettingValue(string id, string value) + { + if (localSettings.TryGetValue(id, out Setting? setting) && setting.GetJsonValue() != value) + { + setting.SetJsonValue(value); + } + } + + public void ApplyButtonClick(string id) + { + if (!localSettings.TryGetValue(id, out Setting? setting) || setting is not Button button) + { + return; + } + + suppressButtonSync = true; + try + { + button.Click(); + } + finally + { + suppressButtonSync = false; + } } private async Task LoadOptions(object instance) @@ -713,16 +763,36 @@ public partial class PluginWindow : Window, IPluginWindow { if (attribute is SettingAttribute elementAttribute) { + localSettings[elementAttribute.Id] = element; + SettingElement settingElement = new SettingElement(element, elementAttribute.Id, elementAttribute.Name, elementAttribute.OrderIndex); if (settings.Settings.Exists(e => e.Id == elementAttribute.Id)) { SettingElement settingsSettingElement = settings.Settings.First(e => e.Id == elementAttribute.Id); - settingElement.JsonValue = settingsSettingElement.JsonValue; + string savedValue = settingsSettingElement.JsonValue; + if (!string.IsNullOrEmpty(savedValue) || element is not Label and not Button) + { + settingElement.JsonValue = savedValue; + } } - element.OnValueChanged += OnPluginSettingValueChanged; - subscribedSettings.Add(element); + Action valueChangedHandler = () => OnPluginSettingValueChanged(element, elementAttribute.Id); + element.OnValueChanged += valueChangedHandler; + subscribedSettings.Add((element, valueChangedHandler)); + + if (element is Button button) + { + Action clickHandler = () => + { + if (!suppressButtonSync) + { + synchronizer?.ButtonClicked(this, elementAttribute.Id); + } + }; + button.OnClick += clickHandler; + subscribedButtonClicks.Add((button, clickHandler)); + } settingElements.Add(settingElement); break; @@ -1070,6 +1140,15 @@ public partial class PluginWindow : Window, IPluginWindow UnsubscribeEvents(); DetachSettings(); + + if (synchronizer is not null && settings.Owner is not null) + { + if (synchronizer.Unregister(this)) + { + Manager.Instance.ReleaseSettingSynchronizer(settings.Owner, PluginMetadata.Id); + } + synchronizer = null; + } } private void DetachSettings() @@ -1098,9 +1177,9 @@ public partial class PluginWindow : Window, IPluginWindow themesCollectionChangedHandler = null; } - foreach (Setting setting in subscribedSettings) + foreach ((Setting Setting, Action Handler) subscription in subscribedSettings) { - setting.OnValueChanged -= OnPluginSettingValueChanged; + subscription.Setting.OnValueChanged -= subscription.Handler; } subscribedSettings.Clear(); @@ -1109,6 +1188,14 @@ public partial class PluginWindow : Window, IPluginWindow subscription.Setting.OnValueChanged -= subscription.Handler; } defaultSettingsSubscriptions.Clear(); + + foreach ((Button Button, Action Handler) subscription in subscribedButtonClicks) + { + subscription.Button.OnClick -= subscription.Handler; + } + subscribedButtonClicks.Clear(); + + localSettings.Clear(); } private void UpdatePosition() diff --git a/src/DesktopMagic/Plugins/SettingSynchronizer.cs b/src/DesktopMagic/Plugins/SettingSynchronizer.cs new file mode 100644 index 0000000..aca1bfe --- /dev/null +++ b/src/DesktopMagic/Plugins/SettingSynchronizer.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Threading; + +namespace DesktopMagic.Plugins; + +/// +/// Keeps the per-instance plugin settings of every window sharing the same layout in sync. +/// Each window owns its own objects (plugins +/// hold them in readonly fields and subscribe to them in Start), so a value change on one +/// window has to be mirrored onto the sibling windows. +/// +internal sealed class SettingSynchronizer +{ + private readonly object _lock = new(); + private readonly List windows = []; + private readonly HashSet<(IPluginWindow Window, string Id)> inFlight = []; + + /// + /// Registers a window so it receives mirrored settings from the other windows of this layout. + /// + public void Register(IPluginWindow window) + { + lock (_lock) + { + if (!windows.Contains(window)) + { + windows.Add(window); + } + } + } + + /// + /// Unregisters a window and returns whether no windows remain. + /// + public bool Unregister(IPluginWindow window) + { + lock (_lock) + { + windows.Remove(window); + return windows.Count == 0; + } + } + + /// + /// Mirrors a setting value change onto every other registered window. + /// + public void SettingChanged(IPluginWindow origin, string id, string value) + { + Mirror(origin, id, window => window.ApplySettingValue(id, value)); + } + + /// + /// Mirrors a button click onto every other registered window. + /// + public void ButtonClicked(IPluginWindow origin, string id) + { + Mirror(origin, id, window => window.ApplyButtonClick(id)); + } + + private void Mirror(IPluginWindow origin, string id, Action apply) + { + IPluginWindow[] snapshot; + lock (_lock) + { + snapshot = windows.ToArray(); + } + + foreach (IPluginWindow window in snapshot) + { + if (ReferenceEquals(window, origin) || !window.IsRunning) + { + continue; + } + + lock (_lock) + { + if (!inFlight.Add((window, id))) + { + continue; + } + } + + if (window is not DispatcherObject dispatcherObject) + { + lock (_lock) + { + inFlight.Remove((window, id)); + } + continue; + } + + _ = dispatcherObject.Dispatcher.InvokeAsync(() => + { + try + { + apply(window); + } + finally + { + lock (_lock) + { + inFlight.Remove((window, id)); + } + } + }); + } + } +} diff --git a/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs b/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs index 2d61343..a33b5c2 100644 --- a/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs +++ b/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs @@ -6,6 +6,7 @@ using DesktopMagic.Settings; using Microsoft.Web.WebView2.Core; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.IO; @@ -41,6 +42,10 @@ public partial class WebPluginWindow : Window, IPluginWindow private readonly List<(Setting Setting, Action Handler)> subscribedSettings = []; private readonly List<(Button Button, Action Handler)> subscribedButtonClicks = []; + private readonly ConcurrentDictionary localSettings = []; + private SettingSynchronizer? synchronizer; + private bool suppressButtonSync = false; + public bool IsRunning { get; private set; } = true; public PluginMetadata PluginMetadata { get; private set; } public string PluginFolderPath { get; private set; } @@ -101,6 +106,12 @@ public partial class WebPluginWindow : Window, IPluginWindow PluginFolderPath = pluginFolderPath; + if (settings.Owner is not null) + { + synchronizer = Manager.Instance.GetSettingSynchronizer(settings.Owner, PluginMetadata.Id); + synchronizer.Register(this); + } + if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath)) { InitializeHotReload(); @@ -157,6 +168,32 @@ public partial class WebPluginWindow : Window, IPluginWindow } } + public void ApplySettingValue(string id, string value) + { + if (localSettings.TryGetValue(id, out Setting? setting) && setting.GetJsonValue() != value) + { + setting.SetJsonValue(value); + } + } + + public void ApplyButtonClick(string id) + { + if (!localSettings.TryGetValue(id, out Setting? setting) || setting is not Button button) + { + return; + } + + suppressButtonSync = true; + try + { + button.Click(); + } + finally + { + suppressButtonSync = false; + } + } + protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); @@ -290,6 +327,15 @@ public partial class WebPluginWindow : Window, IPluginWindow UnsubscribeEvents(); + if (synchronizer is not null && settings.Owner is not null) + { + if (synchronizer.Unregister(this)) + { + Manager.Instance.ReleaseSettingSynchronizer(settings.Owner, PluginMetadata.Id); + } + synchronizer = null; + } + try { if (isInitialized && webView.CoreWebView2 != null) @@ -324,6 +370,8 @@ public partial class WebPluginWindow : Window, IPluginWindow button.OnClick -= handler; } subscribedButtonClicks.Clear(); + + localSettings.Clear(); } private void UpdatePosition() @@ -450,12 +498,18 @@ public partial class WebPluginWindow : Window, IPluginWindow continue; } + localSettings[id] = setting; + SettingElement settingElement = new(setting, id, name, orderIndex); if (settings.Settings.Exists(e => e.Id == id)) { SettingElement saved = settings.Settings.First(e => e.Id == id); - settingElement.JsonValue = saved.JsonValue; + string savedValue = saved.JsonValue; + if (!string.IsNullOrEmpty(savedValue) || setting is not Label and not Button) + { + settingElement.JsonValue = savedValue; + } } string capturedId = id; @@ -463,6 +517,11 @@ public partial class WebPluginWindow : Window, IPluginWindow { Action clickHandler = () => { + if (!suppressButtonSync) + { + synchronizer?.ButtonClicked(this, capturedId); + } + _ = webView.Dispatcher.InvokeAsync(async () => { try @@ -481,6 +540,8 @@ public partial class WebPluginWindow : Window, IPluginWindow Action valueChangedHandler = () => { + synchronizer?.SettingChanged(this, capturedId, setting.GetJsonValue()); + _ = webView.Dispatcher.InvokeAsync(async () => { try diff --git a/src/DesktopMagicPluginAPI/Settings/Button.cs b/src/DesktopMagicPluginAPI/Settings/Button.cs index a2297cc..924db85 100644 --- a/src/DesktopMagicPluginAPI/Settings/Button.cs +++ b/src/DesktopMagicPluginAPI/Settings/Button.cs @@ -46,4 +46,14 @@ public class Button : Setting { OnClick?.Invoke(); } + + internal override string GetJsonValue() + { + return Value; + } + + internal override void SetJsonValue(string value) + { + Value = value; + } } \ No newline at end of file diff --git a/src/DesktopMagicPluginAPI/Settings/Label.cs b/src/DesktopMagicPluginAPI/Settings/Label.cs index 0547c9b..2db66f2 100644 --- a/src/DesktopMagicPluginAPI/Settings/Label.cs +++ b/src/DesktopMagicPluginAPI/Settings/Label.cs @@ -38,4 +38,14 @@ public class Label : Setting Value = value; Bold = bold; } + + internal override string GetJsonValue() + { + return Value; + } + + internal override void SetJsonValue(string value) + { + Value = value; + } } \ No newline at end of file