mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-09-04 08:56:15 +02:00
Fix plugin settings sync when multiple monitors use same layout
This commit is contained in:
@@ -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<Bitmap?> MainAsync(CancellationToken cancellationToken)
|
||||
|
||||
@@ -291,6 +291,32 @@ public sealed class Manager
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
private readonly Dictionary<(Layout, uint), SettingSynchronizer> settingSynchronizers = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets (or creates) the synchronizer that keeps the settings of all windows showing the
|
||||
/// given plugin in the given layout in sync.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops the synchronizer for the given plugin in the given layout once no windows use it anymore.
|
||||
/// </summary>
|
||||
internal void ReleaseSettingSynchronizer(Layout layout, uint pluginId)
|
||||
{
|
||||
_ = settingSynchronizers.Remove((layout, pluginId));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Settings Management
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<Setting> 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<string, Setting> 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;
|
||||
@@ -645,9 +660,16 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
}
|
||||
|
||||
void SetWindowLayer()
|
||||
{
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace DesktopMagic.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the per-instance plugin settings of every window sharing the same layout in sync.
|
||||
/// Each window owns its own <see cref="DesktopMagic.Api.Settings.Setting"/> 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.
|
||||
/// </summary>
|
||||
internal sealed class SettingSynchronizer
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private readonly List<IPluginWindow> windows = [];
|
||||
private readonly HashSet<(IPluginWindow Window, string Id)> inFlight = [];
|
||||
|
||||
/// <summary>
|
||||
/// Registers a window so it receives mirrored settings from the other windows of this layout.
|
||||
/// </summary>
|
||||
public void Register(IPluginWindow window)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!windows.Contains(window))
|
||||
{
|
||||
windows.Add(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a window and returns whether no windows remain.
|
||||
/// </summary>
|
||||
public bool Unregister(IPluginWindow window)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
windows.Remove(window);
|
||||
return windows.Count == 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors a setting value change onto every other registered window.
|
||||
/// </summary>
|
||||
public void SettingChanged(IPluginWindow origin, string id, string value)
|
||||
{
|
||||
Mirror(origin, id, window => window.ApplySettingValue(id, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors a button click onto every other registered window.
|
||||
/// </summary>
|
||||
public void ButtonClicked(IPluginWindow origin, string id)
|
||||
{
|
||||
Mirror(origin, id, window => window.ApplyButtonClick(id));
|
||||
}
|
||||
|
||||
private void Mirror(IPluginWindow origin, string id, Action<IPluginWindow> 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));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, Setting> 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
|
||||
|
||||
@@ -46,4 +46,14 @@ public class Button : Setting
|
||||
{
|
||||
OnClick?.Invoke();
|
||||
}
|
||||
|
||||
internal override string GetJsonValue()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
internal override void SetJsonValue(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user