diff --git a/src/DesktopMagic/Plugins/PluginData.cs b/src/DesktopMagic/Plugins/PluginData.cs index bf1a00a..757903c 100644 --- a/src/DesktopMagic/Plugins/PluginData.cs +++ b/src/DesktopMagic/Plugins/PluginData.cs @@ -46,4 +46,9 @@ internal class PluginData(PluginWindow window, PluginSettings pluginSettings) : { window.UpdatePluginWindow(); } + + public void SaveState() + { + window.SavePluginState(); + } } \ No newline at end of file diff --git a/src/DesktopMagic/Plugins/PluginWindow.xaml.cs b/src/DesktopMagic/Plugins/PluginWindow.xaml.cs index 2929afe..44663a5 100644 --- a/src/DesktopMagic/Plugins/PluginWindow.xaml.cs +++ b/src/DesktopMagic/Plugins/PluginWindow.xaml.cs @@ -14,6 +14,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Loader; +using System.Text.Json; using System.Threading; using System.Timers; using System.Windows; @@ -107,6 +108,14 @@ public partial class PluginWindow : Window, IPluginWindow UpdateTimer_Elapsed(updateTimer, null); } + public void SavePluginState() + { + if (pluginClassInstance is not null) + { + SaveState(pluginClassInstance); + } + } + public void Exit() { IsRunning = false; @@ -258,6 +267,7 @@ public partial class PluginWindow : Window, IPluginWindow return; } + LoadState(pluginClassInstance); LoadOptions(pluginClassInstance); BindDefaultSettings(pluginClassInstance); @@ -401,6 +411,140 @@ public partial class PluginWindow : Window, IPluginWindow } } + private void LoadState(object instance) + { + App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin state", source: "Plugin"); + + try + { + if (settings.State.Count == 0) + { + App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - No state found, using defaults", source: "Plugin"); + return; + } + +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + PropertyInfo[] properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); +#pragma warning restore S3011 + + // Load fields + foreach (FieldInfo field in fields) + { + PersistStateAttribute? persistAttribute = field.GetCustomAttribute(); + if (persistAttribute is null) + { + continue; + } + + if (settings.State.TryGetValue(field.Name, out JsonElement jsonElement)) + { + try + { + object? value = jsonElement.Deserialize(field.FieldType); + field.SetValue(instance, value); + App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Restored state field: {field.Name}", source: "Plugin"); + } + catch (Exception ex) + { + App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to restore field {field.Name}: {ex.Message}", source: "Plugin"); + } + } + } + + // Load properties + foreach (PropertyInfo property in properties) + { + PersistStateAttribute? persistAttribute = property.GetCustomAttribute(); + if (persistAttribute is null || !property.CanWrite) + { + continue; + } + + if (settings.State.TryGetValue(property.Name, out JsonElement jsonElement)) + { + try + { + object? value = jsonElement.Deserialize(property.PropertyType); + property.SetValue(instance, value); + App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Restored state property: {property.Name}", source: "Plugin"); + } + catch (Exception ex) + { + App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to restore property {property.Name}: {ex.Message}", source: "Plugin"); + } + } + } + } + catch (Exception ex) + { + App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error loading state: {ex.Message}", source: "Plugin"); + } + } + + private void SaveState(object instance) + { + App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saving plugin state", source: "Plugin"); + + try + { +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + PropertyInfo[] properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); +#pragma warning restore S3011 + + // Save fields + foreach (FieldInfo field in fields) + { + PersistStateAttribute? persistAttribute = field.GetCustomAttribute(); + if (persistAttribute is null) + { + continue; + } + + try + { + object? value = field.GetValue(instance); + JsonElement jsonElement = JsonSerializer.SerializeToElement(value, field.FieldType); + settings.State[field.Name] = jsonElement; + App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saved state field: {field.Name}", source: "Plugin"); + } + catch (Exception ex) + { + App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to save field {field.Name}: {ex.Message}", source: "Plugin"); + } + } + + // Save properties + foreach (PropertyInfo property in properties) + { + PersistStateAttribute? persistAttribute = property.GetCustomAttribute(); + if (persistAttribute is null || !property.CanRead) + { + continue; + } + + try + { + object? value = property.GetValue(instance); + JsonElement jsonElement = JsonSerializer.SerializeToElement(value, property.PropertyType); + settings.State[property.Name] = jsonElement; + App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saved state property: {property.Name}", source: "Plugin"); + } + catch (Exception ex) + { + App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to save property {property.Name}: {ex.Message}", source: "Plugin"); + } + } + + App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - State saved successfully ({settings.State.Count} items)", source: "Plugin"); + } + catch (Exception ex) + { + App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error saving state: {ex.Message}", source: "Plugin"); + } + } + private async void UpdateTimer_Elapsed(object? sender, ElapsedEventArgs? e) { try @@ -479,7 +623,11 @@ public partial class PluginWindow : Window, IPluginWindow try { - pluginClassInstance?.Stop(); + if (pluginClassInstance is not null) + { + SaveState(pluginClassInstance); + pluginClassInstance.Stop(); + } } catch (Exception ex) { diff --git a/src/DesktopMagic/Settings/PluginSettings.cs b/src/DesktopMagic/Settings/PluginSettings.cs index 8261f7b..0e67497 100644 --- a/src/DesktopMagic/Settings/PluginSettings.cs +++ b/src/DesktopMagic/Settings/PluginSettings.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; +using System.Text.Json; using System.Text.Json.Serialization; using System.Windows; @@ -16,6 +17,7 @@ public class PluginSettings : INotifyPropertyChanged private string? currentThemeName; private List settings = []; + private Dictionary state = []; private bool enabled = false; private Point position = new Point(100, 100); private Point size = new Point(300, 300); @@ -61,6 +63,23 @@ public class PluginSettings : INotifyPropertyChanged } } + /// + /// Dictionary storing plugin state. Key is the field/property name, value is the JsonElement. + /// This is persisted per layout in settings.json. + /// + public Dictionary State + { + get => state; + set + { + if (state != value) + { + state = value; + OnPropertyChanged(); + } + } + } + public bool Enabled { get => enabled; diff --git a/src/DesktopMagicPluginAPI/IPluginData.cs b/src/DesktopMagicPluginAPI/IPluginData.cs index e9ba593..7ce2a07 100644 --- a/src/DesktopMagicPluginAPI/IPluginData.cs +++ b/src/DesktopMagicPluginAPI/IPluginData.cs @@ -50,4 +50,13 @@ public interface IPluginData /// The message to display. /// The title of the message box. void ShowMessage(string message, string title); + + /// + /// Saves the current state of fields and properties marked with + /// + /// + /// State is automatically saved when the plugin stops, but this method can be called + /// to save state at any time, such as after important user interactions. + /// + void SaveState(); } \ No newline at end of file diff --git a/src/DesktopMagicPluginAPI/PersistStateAttribute.cs b/src/DesktopMagicPluginAPI/PersistStateAttribute.cs new file mode 100644 index 0000000..b5420a7 --- /dev/null +++ b/src/DesktopMagicPluginAPI/PersistStateAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace DesktopMagic.Api; + +/// +/// Marks a field or property to be automatically persisted between plugin sessions. +/// +/// +/// The field or property must be JSON serializable. Complex types should have parameterless constructors. +/// State is saved when the plugin stops and loaded when it starts. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public sealed class PersistStateAttribute : Attribute +{ +}