diff --git a/src/SharpFM.Plugin.Sample/ClipInspectorPanel.axaml b/src/SharpFM.Plugin.Sample/ClipInspectorPanel.axaml
index 2cdf87a..6730aa5 100644
--- a/src/SharpFM.Plugin.Sample/ClipInspectorPanel.axaml
+++ b/src/SharpFM.Plugin.Sample/ClipInspectorPanel.axaml
@@ -19,12 +19,12 @@
-
+
-
+
diff --git a/src/SharpFM.Plugin.Sample/ClipInspectorPlugin.cs b/src/SharpFM.Plugin.Sample/ClipInspectorPlugin.cs
index 98d1274..22685d5 100644
--- a/src/SharpFM.Plugin.Sample/ClipInspectorPlugin.cs
+++ b/src/SharpFM.Plugin.Sample/ClipInspectorPlugin.cs
@@ -17,8 +17,37 @@ public class ClipInspectorPlugin : IPanelPlugin
public IReadOnlyList KeyBindings => [];
public IReadOnlyList MenuActions => [];
+ public PluginConfigSchema ConfigSchema { get; } = new(new[]
+ {
+ new PluginConfigField(
+ Key: "ShowElementCount",
+ Label: "Show XML element count",
+ Type: PluginConfigFieldType.Bool,
+ DefaultValue: true,
+ Description: "Display the number of XML elements in the selected clip."),
+ new PluginConfigField(
+ Key: "ShowXmlSize",
+ Label: "Show approximate size",
+ Type: PluginConfigFieldType.Bool,
+ DefaultValue: true,
+ Description: "Display the approximate byte size of the clip's XML."),
+ });
+
+ public void OnConfigChanged(IReadOnlyDictionary values)
+ {
+ _showElementCount = values.TryGetValue("ShowElementCount", out var a) && a is bool ba ? ba : true;
+ _showXmlSize = values.TryGetValue("ShowXmlSize", out var b) && b is bool bb ? bb : true;
+ if (_viewModel is not null)
+ {
+ _viewModel.ShowElementCount = _showElementCount;
+ _viewModel.ShowXmlSize = _showXmlSize;
+ }
+ }
+
private IPluginHost? _host;
private ClipInspectorViewModel? _viewModel;
+ private bool _showElementCount = true;
+ private bool _showXmlSize = true;
public void Initialize(IPluginHost host)
{
@@ -29,7 +58,11 @@ public void Initialize(IPluginHost host)
public Control CreatePanel()
{
- _viewModel = new ClipInspectorViewModel();
+ _viewModel = new ClipInspectorViewModel
+ {
+ ShowElementCount = _showElementCount,
+ ShowXmlSize = _showXmlSize,
+ };
_viewModel.Update(_host?.SelectedClip);
return new ClipInspectorPanel { DataContext = _viewModel };
}
diff --git a/src/SharpFM.Plugin.Sample/ClipInspectorViewModel.cs b/src/SharpFM.Plugin.Sample/ClipInspectorViewModel.cs
index fb3ad96..e4460f5 100644
--- a/src/SharpFM.Plugin.Sample/ClipInspectorViewModel.cs
+++ b/src/SharpFM.Plugin.Sample/ClipInspectorViewModel.cs
@@ -30,6 +30,12 @@ private void Notify([CallerMemberName] string name = "")
private bool _hasClip;
public bool HasClip { get => _hasClip; private set { _hasClip = value; Notify(); } }
+ private bool _showElementCount = true;
+ public bool ShowElementCount { get => _showElementCount; set { _showElementCount = value; Notify(); } }
+
+ private bool _showXmlSize = true;
+ public bool ShowXmlSize { get => _showXmlSize; set { _showXmlSize = value; Notify(); } }
+
public void Update(ClipData? clip)
{
if (clip is null)
diff --git a/src/SharpFM.Plugin.XmlViewer/XmlViewerPlugin.cs b/src/SharpFM.Plugin.XmlViewer/XmlViewerPlugin.cs
index 8e2af88..a773a8c 100644
--- a/src/SharpFM.Plugin.XmlViewer/XmlViewerPlugin.cs
+++ b/src/SharpFM.Plugin.XmlViewer/XmlViewerPlugin.cs
@@ -22,6 +22,8 @@ public class XmlViewerPlugin : IPanelPlugin
[new PluginKeyBinding("Ctrl+Shift+X", "Toggle XML Viewer", () => { })];
public IReadOnlyList MenuActions => [];
+ public PluginConfigSchema ConfigSchema => PluginConfigSchema.Empty;
+ public void OnConfigChanged(IReadOnlyDictionary values) { }
public void Initialize(IPluginHost host)
{
diff --git a/src/SharpFM.Plugin/IPlugin.cs b/src/SharpFM.Plugin/IPlugin.cs
index 5788939..dcf1105 100644
--- a/src/SharpFM.Plugin/IPlugin.cs
+++ b/src/SharpFM.Plugin/IPlugin.cs
@@ -46,4 +46,20 @@ public interface IPlugin : IDisposable
/// as a submenu with "Toggle Panel" plus these custom actions.
///
IReadOnlyList MenuActions { get; }
+
+ ///
+ /// Schema for user-tunable configuration values. The host persists these on the
+ /// plugin's behalf and renders a generic settings UI from the schema. Return
+ /// if the plugin has no configuration.
+ ///
+ PluginConfigSchema ConfigSchema { get; }
+
+ ///
+ /// Called by the host with the current values for fields declared in
+ /// : once after with the
+ /// persisted (or default) values, and again whenever the user saves edits in the
+ /// Plugin Manager. Keys match ; values are
+ /// coerced to the CLR type implied by .
+ ///
+ void OnConfigChanged(IReadOnlyDictionary values);
}
diff --git a/src/SharpFM.Plugin/PluginConfigSchema.cs b/src/SharpFM.Plugin/PluginConfigSchema.cs
new file mode 100644
index 0000000..b801fcb
--- /dev/null
+++ b/src/SharpFM.Plugin/PluginConfigSchema.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+
+namespace SharpFM.Plugin;
+
+///
+/// Supported field types for a plugin configuration schema. The host renders a
+/// different control per type and coerces persisted values to the matching CLR type.
+///
+public enum PluginConfigFieldType
+{
+ String,
+ MultilineString,
+ Bool,
+ Int,
+ Double,
+ Enum
+}
+
+///
+/// A single configurable value declared by a plugin.
+///
+/// Dictionary key used when values are passed back to the plugin.
+/// Human-readable label shown in the settings UI.
+/// Field type — determines the rendered control and value coercion.
+///
+/// Value returned when the field has never been set or the persisted value is invalid.
+/// Must be assignable to the CLR type implied by .
+///
+/// Optional caption shown beneath the control.
+/// Allowed values when is .
+public sealed record PluginConfigField(
+ string Key,
+ string Label,
+ PluginConfigFieldType Type,
+ object? DefaultValue = null,
+ string? Description = null,
+ IReadOnlyList? EnumValues = null);
+
+///
+/// Describes a plugin's user-tunable configuration. The host uses this to
+/// persist values, generate a settings UI, and push the current values into the plugin.
+///
+public sealed record PluginConfigSchema(IReadOnlyList Fields)
+{
+ /// Schema for plugins with no configuration.
+ public static PluginConfigSchema Empty { get; } = new(Array.Empty());
+}
diff --git a/src/SharpFM/App.axaml.cs b/src/SharpFM/App.axaml.cs
index 22b3ea3..dd02f7f 100644
--- a/src/SharpFM/App.axaml.cs
+++ b/src/SharpFM/App.axaml.cs
@@ -49,7 +49,8 @@ public override void OnFrameworkInitializationCompleted()
// Load plugins
var pluginHost = new PluginHost(viewModel, loggerFactory);
var pluginUIHost = new PluginUIHost(pluginHost);
- var pluginService = new PluginService(logger);
+ var pluginConfigService = new PluginConfigService(logger);
+ var pluginService = new PluginService(logger, pluginConfigService);
pluginService.LoadPlugins(pluginUIHost);
viewModel.AllPlugins = pluginService.AllPlugins;
@@ -62,7 +63,7 @@ public override void OnFrameworkInitializationCompleted()
// Give the window access to plugin services for the manager dialog
if (desktop.MainWindow is MainWindow mainWindow)
- mainWindow.SetPluginServices(pluginService, pluginUIHost);
+ mainWindow.SetPluginServices(pluginService, pluginUIHost, pluginConfigService);
desktop.MainWindow.DataContext = viewModel;
diff --git a/src/SharpFM/MainWindow.axaml.cs b/src/SharpFM/MainWindow.axaml.cs
index 8fc40aa..0927958 100644
--- a/src/SharpFM/MainWindow.axaml.cs
+++ b/src/SharpFM/MainWindow.axaml.cs
@@ -26,6 +26,7 @@ public partial class MainWindow : Window
private TextMate.Installation? _scriptTextMateInstallation;
private PluginService? _pluginService;
private PluginUIHost? _pluginHost;
+ private PluginConfigService? _pluginConfigService;
public MainWindow()
{
@@ -58,10 +59,11 @@ public MainWindow()
DataContextChanged += OnDataContextChanged;
}
- public void SetPluginServices(PluginService pluginService, PluginUIHost pluginHost)
+ public void SetPluginServices(PluginService pluginService, PluginUIHost pluginHost, PluginConfigService pluginConfigService)
{
_pluginService = pluginService;
_pluginHost = pluginHost;
+ _pluginConfigService = pluginConfigService;
}
private void OnDataContextChanged(object? sender, EventArgs e)
@@ -229,11 +231,11 @@ private void UpdatePluginPanelContent()
private void ShowPluginManager()
{
- if (_pluginService is null || _pluginHost is null) return;
+ if (_pluginService is null || _pluginHost is null || _pluginConfigService is null) return;
if (DataContext is not MainWindowViewModel vm) return;
var window = new PluginManagerWindow();
- window.Configure(_pluginService, _pluginHost, vm);
+ window.Configure(_pluginService, _pluginHost, vm, _pluginConfigService);
window.ShowDialog(this);
}
diff --git a/src/SharpFM/PluginManager/PluginConfigDialog.axaml b/src/SharpFM/PluginManager/PluginConfigDialog.axaml
new file mode 100644
index 0000000..80c6e6a
--- /dev/null
+++ b/src/SharpFM/PluginManager/PluginConfigDialog.axaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SharpFM/PluginManager/PluginConfigDialog.axaml.cs b/src/SharpFM/PluginManager/PluginConfigDialog.axaml.cs
new file mode 100644
index 0000000..756da37
--- /dev/null
+++ b/src/SharpFM/PluginManager/PluginConfigDialog.axaml.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Markup.Xaml;
+using SharpFM.Plugin;
+
+namespace SharpFM.PluginManager;
+
+[ExcludeFromCodeCoverage]
+public partial class PluginConfigDialog : Window
+{
+ private readonly Dictionary> _readers = new(StringComparer.Ordinal);
+ private Dictionary? _result;
+
+ public PluginConfigDialog()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent() => AvaloniaXamlLoader.Load(this);
+
+ public static async System.Threading.Tasks.Task?> ShowAsync(
+ Window owner,
+ string pluginDisplayName,
+ PluginConfigSchema schema,
+ IReadOnlyDictionary currentValues)
+ {
+ var dialog = new PluginConfigDialog();
+ dialog.Build(pluginDisplayName, schema, currentValues);
+ await dialog.ShowDialog(owner);
+ return dialog._result;
+ }
+
+ private void Build(string pluginDisplayName, PluginConfigSchema schema, IReadOnlyDictionary currentValues)
+ {
+ Title = $"Configure {pluginDisplayName}";
+ var header = this.FindControl("headerText");
+ if (header is not null) header.Text = pluginDisplayName;
+
+ var panel = this.FindControl("fieldsPanel");
+ if (panel is not null)
+ {
+ foreach (var field in schema.Fields)
+ {
+ currentValues.TryGetValue(field.Key, out var current);
+ panel.Children.Add(BuildFieldControl(field, current));
+ }
+ }
+
+ var ok = this.FindControl