diff --git a/SharpFM.sln b/SharpFM.sln index 910ff1a..b9b996e 100644 --- a/SharpFM.sln +++ b/SharpFM.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM.Plugin.XmlViewer", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM.Model", "src\SharpFM.Model\SharpFM.Model.csproj", "{E0FF2DD8-E4B8-4495-92FA-F17AF9B78086}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM.Plugin.UI", "src\SharpFM.Plugin.UI\SharpFM.Plugin.UI.csproj", "{8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -115,6 +117,18 @@ Global {E0FF2DD8-E4B8-4495-92FA-F17AF9B78086}.Release|x64.Build.0 = Release|Any CPU {E0FF2DD8-E4B8-4495-92FA-F17AF9B78086}.Release|x86.ActiveCfg = Release|Any CPU {E0FF2DD8-E4B8-4495-92FA-F17AF9B78086}.Release|x86.Build.0 = Release|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Debug|x64.ActiveCfg = Debug|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Debug|x64.Build.0 = Debug|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Debug|x86.ActiveCfg = Debug|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Debug|x86.Build.0 = Debug|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Release|Any CPU.Build.0 = Release|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Release|x64.ActiveCfg = Release|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Release|x64.Build.0 = Release|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Release|x86.ActiveCfg = Release|Any CPU + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -126,5 +140,6 @@ Global {74337D8E-5EC6-4E5F-9E9E-F2B59E8ECABB} = {E2FF2BB3-AF37-44BA-BD84-999B352D814E} {E988ECF3-E096-4F29-88C0-27B50FD6C703} = {1515B0F2-1419-4778-92A8-430A8B4931F7} {E0FF2DD8-E4B8-4495-92FA-F17AF9B78086} = {1515B0F2-1419-4778-92A8-430A8B4931F7} + {8DDBA57D-48E1-400C-A9F6-F02DEFD35EFE} = {1515B0F2-1419-4778-92A8-430A8B4931F7} EndGlobalSection EndGlobal diff --git a/src/SharpFM.Plugin.Sample/ClipInspectorPlugin.cs b/src/SharpFM.Plugin.Sample/ClipInspectorPlugin.cs index 4802fe8..98d1274 100644 --- a/src/SharpFM.Plugin.Sample/ClipInspectorPlugin.cs +++ b/src/SharpFM.Plugin.Sample/ClipInspectorPlugin.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using SharpFM.Model; using SharpFM.Plugin; +using SharpFM.Plugin.UI; namespace SharpFM.Plugin.Sample; @@ -11,6 +12,7 @@ public class ClipInspectorPlugin : IPanelPlugin { public string Id => "clip-inspector"; public string DisplayName => "Clip Inspector"; + public string Description => "Displays clip metadata including name, type, element count, and size."; public string Version => GetType().Assembly.GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; public IReadOnlyList KeyBindings => []; public IReadOnlyList MenuActions => []; diff --git a/src/SharpFM.Plugin.Sample/SharpFM.Plugin.Sample.csproj b/src/SharpFM.Plugin.Sample/SharpFM.Plugin.Sample.csproj index f0fa2dc..b3e49c6 100644 --- a/src/SharpFM.Plugin.Sample/SharpFM.Plugin.Sample.csproj +++ b/src/SharpFM.Plugin.Sample/SharpFM.Plugin.Sample.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/SharpFM.Plugin/IPanelPlugin.cs b/src/SharpFM.Plugin.UI/IPanelPlugin.cs similarity index 93% rename from src/SharpFM.Plugin/IPanelPlugin.cs rename to src/SharpFM.Plugin.UI/IPanelPlugin.cs index 6135cad..12f8123 100644 --- a/src/SharpFM.Plugin/IPanelPlugin.cs +++ b/src/SharpFM.Plugin.UI/IPanelPlugin.cs @@ -1,6 +1,6 @@ using Avalonia.Controls; -namespace SharpFM.Plugin; +namespace SharpFM.Plugin.UI; /// /// A plugin that provides a dockable side panel in the SharpFM UI. diff --git a/src/SharpFM.Plugin.UI/IPluginUIHost.cs b/src/SharpFM.Plugin.UI/IPluginUIHost.cs new file mode 100644 index 0000000..0dfdd4f --- /dev/null +++ b/src/SharpFM.Plugin.UI/IPluginUIHost.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Avalonia.Controls; + +namespace SharpFM.Plugin.UI; + +/// +/// Extended host interface for plugins that need UI capabilities beyond +/// the simple dialogs on . Provides content +/// dialog hosting backed by Avalonia. +/// +public interface IPluginUIHost : IPluginHost +{ + /// + /// Show a modal dialog containing plugin-provided content. + /// Returns true if the user accepted (OK), false if cancelled. + /// + Task ShowContentDialogAsync(string title, Control content); +} diff --git a/src/SharpFM.Plugin.UI/SharpFM.Plugin.UI.csproj b/src/SharpFM.Plugin.UI/SharpFM.Plugin.UI.csproj new file mode 100644 index 0000000..d1779c5 --- /dev/null +++ b/src/SharpFM.Plugin.UI/SharpFM.Plugin.UI.csproj @@ -0,0 +1,25 @@ + + + net10.0 + enable + latest + + + + 2.0 + v + beta.0 + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/src/SharpFM.Plugin.XmlViewer/SharpFM.Plugin.XmlViewer.csproj b/src/SharpFM.Plugin.XmlViewer/SharpFM.Plugin.XmlViewer.csproj index db1afb4..a2db180 100644 --- a/src/SharpFM.Plugin.XmlViewer/SharpFM.Plugin.XmlViewer.csproj +++ b/src/SharpFM.Plugin.XmlViewer/SharpFM.Plugin.XmlViewer.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/SharpFM.Plugin.XmlViewer/XmlViewerPlugin.cs b/src/SharpFM.Plugin.XmlViewer/XmlViewerPlugin.cs index 66a2d46..8e2af88 100644 --- a/src/SharpFM.Plugin.XmlViewer/XmlViewerPlugin.cs +++ b/src/SharpFM.Plugin.XmlViewer/XmlViewerPlugin.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using SharpFM.Model; using SharpFM.Plugin; +using SharpFM.Plugin.UI; namespace SharpFM.Plugin.XmlViewer; @@ -11,6 +12,7 @@ public class XmlViewerPlugin : IPanelPlugin { public string Id => "xml-viewer"; public string DisplayName => "XML Viewer"; + public string Description => "Live XML panel with syntax highlighting and bidirectional sync."; public string Version => GetType().Assembly.GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; private IPluginHost? _host; diff --git a/src/SharpFM.Plugin/ClipContentChangedArgs.cs b/src/SharpFM.Plugin/ClipContentChangedArgs.cs index 18f4f66..dc5b4f0 100644 --- a/src/SharpFM.Plugin/ClipContentChangedArgs.cs +++ b/src/SharpFM.Plugin/ClipContentChangedArgs.cs @@ -6,6 +6,6 @@ namespace SharpFM.Plugin; /// Event arguments for . /// /// Fresh snapshot of the clip with synced XML. -/// "editor" for user edits, or the originating plugin's . +/// "editor" for user edits, or the originating plugin's . /// True if the XML was produced from an incomplete parse (e.g. mid-typing). public record ClipContentChangedArgs(ClipData Clip, string Origin, bool IsPartial); diff --git a/src/SharpFM.Plugin/IClipTransformPlugin.cs b/src/SharpFM.Plugin/IClipTransform.cs similarity index 63% rename from src/SharpFM.Plugin/IClipTransformPlugin.cs rename to src/SharpFM.Plugin/IClipTransform.cs index 56a5202..816427a 100644 --- a/src/SharpFM.Plugin/IClipTransformPlugin.cs +++ b/src/SharpFM.Plugin/IClipTransform.cs @@ -3,11 +3,10 @@ namespace SharpFM.Plugin; /// -/// A plugin that transforms clip XML during import or export operations. -/// Transforms run in plugin load order. Use to allow -/// users to toggle transforms without uninstalling. +/// A clip transform that runs during import/export operations. +/// Register instances with . /// -public interface IClipTransformPlugin : IPlugin +public interface IClipTransform { /// /// Transform clip XML when a clip is imported (pasted from FileMaker or loaded from storage). @@ -22,8 +21,7 @@ public interface IClipTransformPlugin : IPlugin Task OnExportAsync(string clipType, string xml); /// - /// Whether this transformer is currently enabled. - /// The user can toggle transformers on/off without uninstalling them. + /// Whether this transform is currently enabled. /// bool IsEnabled { get; set; } } diff --git a/src/SharpFM.Plugin/IEventPlugin.cs b/src/SharpFM.Plugin/IEventPlugin.cs deleted file mode 100644 index c81d464..0000000 --- a/src/SharpFM.Plugin/IEventPlugin.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SharpFM.Plugin; - -/// -/// A headless plugin that reacts to host events with no UI panel. -/// Subscribe to events in -/// and unsubscribe in . -/// -public interface IEventPlugin : IPlugin { } diff --git a/src/SharpFM.Plugin/IPersistencePlugin.cs b/src/SharpFM.Plugin/IPersistencePlugin.cs deleted file mode 100644 index 49c59d6..0000000 --- a/src/SharpFM.Plugin/IPersistencePlugin.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SharpFM.Model; - -namespace SharpFM.Plugin; - -/// -/// A plugin that provides an alternative clip storage backend. -/// The host registers this as an available persistence provider -/// that users can switch to via the UI. -/// -public interface IPersistencePlugin : IPlugin -{ - /// - /// Create and return the repository instance for this storage backend. - /// Called when the user selects this provider. - /// - IClipRepository CreateRepository(); -} diff --git a/src/SharpFM.Plugin/IPlugin.cs b/src/SharpFM.Plugin/IPlugin.cs index 2752ec2..5788939 100644 --- a/src/SharpFM.Plugin/IPlugin.cs +++ b/src/SharpFM.Plugin/IPlugin.cs @@ -18,6 +18,11 @@ public interface IPlugin : IDisposable /// string DisplayName { get; } + /// + /// Short description of what this plugin does, shown in the Plugin Manager UI. + /// + string Description { get; } + /// /// Plugin version string (e.g. "2.0.0-beta.0"). Shown in the Plugin Manager UI. /// diff --git a/src/SharpFM.Plugin/IPluginHost.cs b/src/SharpFM.Plugin/IPluginHost.cs index 72f54f1..8a8e4fe 100644 --- a/src/SharpFM.Plugin/IPluginHost.cs +++ b/src/SharpFM.Plugin/IPluginHost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using SharpFM.Model; using SharpFM.Model.Schema; @@ -77,4 +78,28 @@ public interface IPluginHost /// Remove a clip from the loaded collection by name. /// bool RemoveClip(string clipName); + + /// + /// Register a clip repository provided by this plugin. + /// The host adds it to the list of available storage backends. + /// + void RegisterRepository(IClipRepository repository); + + /// + /// Register a clip transform provided by this plugin. + /// Transforms run during import/export in registration order. + /// + void RegisterTransform(IClipTransform transform); + + /// + /// Show a simple modal dialog with a message and buttons. + /// Returns the label of the clicked button, or null if cancelled. + /// + Task ShowDialogAsync(string title, string message, string[] buttons); + + /// + /// Show a modal input dialog prompting the user for text. + /// Returns the entered text, or null if cancelled. + /// + Task ShowInputDialogAsync(string title, string prompt, string? defaultValue = null); } diff --git a/src/SharpFM.Plugin/SharpFM.Plugin.csproj b/src/SharpFM.Plugin/SharpFM.Plugin.csproj index 0f0b752..44b2030 100644 --- a/src/SharpFM.Plugin/SharpFM.Plugin.csproj +++ b/src/SharpFM.Plugin/SharpFM.Plugin.csproj @@ -16,7 +16,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/SharpFM/App.axaml.cs b/src/SharpFM/App.axaml.cs index c4c2148..22b3ea3 100644 --- a/src/SharpFM/App.axaml.cs +++ b/src/SharpFM/App.axaml.cs @@ -48,22 +48,21 @@ public override void OnFrameworkInitializationCompleted() // Load plugins var pluginHost = new PluginHost(viewModel, loggerFactory); + var pluginUIHost = new PluginUIHost(pluginHost); var pluginService = new PluginService(logger); - pluginService.LoadPlugins(pluginHost); + pluginService.LoadPlugins(pluginUIHost); - // Wire up all plugin types - viewModel.PanelPlugins = pluginService.PanelPlugins; - viewModel.TransformPlugins = pluginService.TransformPlugins; + viewModel.AllPlugins = pluginService.AllPlugins; + viewModel.PluginUI = pluginUIHost; - // Build repository list: built-in + plugin-provided + // Build repository list: built-in + plugin-registered var repos = new List { viewModel.ActiveRepository }; - foreach (var pp in pluginService.PersistencePlugins) - repos.Add(pp.CreateRepository()); + repos.AddRange(pluginHost.Repositories); viewModel.AvailableRepositories = repos; // Give the window access to plugin services for the manager dialog if (desktop.MainWindow is MainWindow mainWindow) - mainWindow.SetPluginServices(pluginService, pluginHost); + mainWindow.SetPluginServices(pluginService, pluginUIHost); desktop.MainWindow.DataContext = viewModel; diff --git a/src/SharpFM/MainWindow.axaml.cs b/src/SharpFM/MainWindow.axaml.cs index 63dad48..8fc40aa 100644 --- a/src/SharpFM/MainWindow.axaml.cs +++ b/src/SharpFM/MainWindow.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using Avalonia; @@ -8,6 +9,7 @@ using AvaloniaEdit.TextMate; using SharpFM.Diagnostics; using SharpFM.Plugin; +using SharpFM.Plugin.UI; using SharpFM.PluginManager; using SharpFM.Scripting; using SharpFM.Services; @@ -23,7 +25,7 @@ public partial class MainWindow : Window private ScriptEditorController? _scriptController; private TextMate.Installation? _scriptTextMateInstallation; private PluginService? _pluginService; - private IPluginHost? _pluginHost; + private PluginUIHost? _pluginHost; public MainWindow() { @@ -56,7 +58,7 @@ public MainWindow() DataContextChanged += OnDataContextChanged; } - public void SetPluginServices(PluginService pluginService, IPluginHost pluginHost) + public void SetPluginServices(PluginService pluginService, PluginUIHost pluginHost) { _pluginService = pluginService; _pluginHost = pluginHost; @@ -68,16 +70,23 @@ private void OnDataContextChanged(object? sender, EventArgs e) BuildPluginMenuItems(vm); vm.PropertyChanged += OnViewModelPropertyChanged; + + if (vm.PluginUI is { } pluginUI) + pluginUI.PropertyChanged += OnPluginUIPropertyChanged; } private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(MainWindowViewModel.IsPluginPanelVisible)) + if (e.PropertyName == nameof(MainWindowViewModel.SelectedClip)) + AttachScriptClipEditorIfApplicable(); + } + + private void OnPluginUIPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(PluginUIHost.IsVisible)) UpdatePluginPanelVisibility(); - else if (e.PropertyName == nameof(MainWindowViewModel.PluginPanelControl)) + else if (e.PropertyName == nameof(PluginUIHost.PanelControl)) UpdatePluginPanelContent(); - else if (e.PropertyName == nameof(MainWindowViewModel.SelectedClip)) - AttachScriptClipEditorIfApplicable(); } private void OnScriptControllerStatusMessage(object? sender, SharpFM.Scripting.Editor.StatusMessageEventArgs e) @@ -100,32 +109,47 @@ private void BuildPluginMenuItems(MainWindowViewModel vm) { var pluginsMenu = this.FindControl("pluginsMenu"); var manageItem = this.FindControl("managePluginsMenuItem"); - if (pluginsMenu is null || manageItem is null || vm.PanelPlugins.Count == 0) + var pluginUI = vm.PluginUI; + if (pluginsMenu is null || manageItem is null || vm.AllPlugins.Count == 0) { RegisterPluginKeyBindings(vm); return; } - // Insert plugin items before the Manage Plugins item var insertIndex = pluginsMenu.Items.IndexOf(manageItem); - foreach (var plugin in vm.PanelPlugins) + foreach (var plugin in vm.AllPlugins) { + var isPanel = pluginUI?.HasPanel(plugin) ?? false; + var hasActions = plugin.MenuActions.Count > 0; + + if (!isPanel && !hasActions) continue; + MenuItem pluginItem; - if (plugin.MenuActions.Count > 0) + if (isPanel && !hasActions) + { + // Panel with no custom actions — flat item that toggles the panel + pluginItem = new MenuItem { Header = plugin.DisplayName, Tag = plugin }; + if (plugin.KeyBindings.Count > 0) + pluginItem.InputGesture = KeyGesture.Parse(plugin.KeyBindings[0].Gesture); + var p = plugin; + pluginItem.Click += (_, _) => pluginUI?.TogglePanel(p); + } + else { - // Plugin has custom actions — create a submenu + // Submenu with actions (and toggle item for panels) pluginItem = new MenuItem { Header = plugin.DisplayName }; - var toggleItem = new MenuItem { Header = "Toggle Panel", Tag = plugin }; - if (plugin.KeyBindings.Count > 0) - toggleItem.InputGesture = KeyGesture.Parse(plugin.KeyBindings[0].Gesture); - toggleItem.Click += (_, _) => + if (isPanel) { - if (toggleItem.Tag is IPanelPlugin p) vm.TogglePluginPanel(p); - }; - pluginItem.Items.Add(toggleItem); + var toggleItem = new MenuItem { Header = "Toggle Panel" }; + if (plugin.KeyBindings.Count > 0) + toggleItem.InputGesture = KeyGesture.Parse(plugin.KeyBindings[0].Gesture); + var p = plugin; + toggleItem.Click += (_, _) => pluginUI?.TogglePanel(p); + pluginItem.Items.Add(toggleItem); + } foreach (var action in plugin.MenuActions) { @@ -137,42 +161,34 @@ private void BuildPluginMenuItems(MainWindowViewModel vm) pluginItem.Items.Add(actionItem); } } - else - { - // No custom actions — flat item that toggles the panel - pluginItem = new MenuItem { Header = plugin.DisplayName, Tag = plugin }; - if (plugin.KeyBindings.Count > 0) - pluginItem.InputGesture = KeyGesture.Parse(plugin.KeyBindings[0].Gesture); - pluginItem.Click += (_, _) => - { - if (pluginItem.Tag is IPanelPlugin p) vm.TogglePluginPanel(p); - }; - } pluginsMenu.Items.Insert(insertIndex++, pluginItem); } - // Add separator before Manage Plugins pluginsMenu.Items.Insert(insertIndex, new Separator()); - RegisterPluginKeyBindings(vm); } private void RegisterPluginKeyBindings(MainWindowViewModel vm) { - foreach (var plugin in vm.PanelPlugins) + var pluginUI = vm.PluginUI; + + foreach (var plugin in vm.AllPlugins) { foreach (var binding in plugin.KeyBindings) { var gesture = KeyGesture.Parse(binding.Gesture); - var pluginRef = plugin; + var p = plugin; + var cb = binding.Callback; + KeyBindings.Add(new KeyBinding { Gesture = gesture, Command = new PluginKeyCommand(() => { - vm.TogglePluginPanel(pluginRef); - binding.Callback(); + if (pluginUI?.HasPanel(p) == true) + pluginUI.TogglePanel(p); + cb(); }) }); } @@ -195,7 +211,7 @@ private void UpdatePluginPanelVisibility() { if (DataContext is not MainWindowViewModel vm) return; - var visible = vm.IsPluginPanelVisible; + var visible = vm.PluginUI?.IsVisible ?? false; pluginSplitter.IsVisible = visible; pluginPanelBorder.IsVisible = visible; editorPluginGrid.ColumnDefinitions[1].Width = visible ? new GridLength(16) : new GridLength(0); @@ -208,7 +224,7 @@ private void UpdatePluginPanelContent() var host = this.FindControl("pluginPanelHost"); if (host is not null) - host.Content = vm.PluginPanelControl; + host.Content = vm.PluginUI?.PanelControl; } private void ShowPluginManager() diff --git a/src/SharpFM/PluginManager/PluginManagerViewModel.cs b/src/SharpFM/PluginManager/PluginManagerViewModel.cs index 4d7ca76..2252bcf 100644 --- a/src/SharpFM/PluginManager/PluginManagerViewModel.cs +++ b/src/SharpFM/PluginManager/PluginManagerViewModel.cs @@ -15,18 +15,10 @@ private void Notify([CallerMemberName] string name = "") => public IPlugin Plugin { get; } public string Id => Plugin.Id; public string DisplayName => Plugin.DisplayName; + public string Description => Plugin.Description; public string PluginVersion => Plugin.Version; public string AssemblyName => Plugin.GetType().Assembly.GetName().Name ?? "(unknown)"; - public string PluginType => Plugin switch - { - IPanelPlugin => "Panel", - IEventPlugin => "Event", - IPersistencePlugin => "Storage", - IClipTransformPlugin => "Transform", - _ => "Unknown" - }; - private bool _isActive; public bool IsActive { @@ -58,12 +50,12 @@ public PluginEntry? SelectedPlugin public bool HasSelection => _selectedPlugin is not null; - public void Refresh(IReadOnlyList allPlugins, IPanelPlugin? activePlugin) + public void Refresh(IReadOnlyList allPlugins, string? activePluginId) { Plugins.Clear(); foreach (var plugin in allPlugins) { - Plugins.Add(new PluginEntry(plugin, plugin.Id == activePlugin?.Id)); + Plugins.Add(new PluginEntry(plugin, plugin.Id == activePluginId)); } } } diff --git a/src/SharpFM/PluginManager/PluginManagerWindow.axaml.cs b/src/SharpFM/PluginManager/PluginManagerWindow.axaml.cs index 0172485..49bb5f6 100644 --- a/src/SharpFM/PluginManager/PluginManagerWindow.axaml.cs +++ b/src/SharpFM/PluginManager/PluginManagerWindow.axaml.cs @@ -15,7 +15,7 @@ public partial class PluginManagerWindow : Window { private readonly PluginManagerViewModel _viewModel = new(); private PluginService? _pluginService; - private IPluginHost? _host; + private PluginUIHost? _host; private MainWindowViewModel? _mainVm; public PluginManagerWindow() @@ -32,12 +32,12 @@ public PluginManagerWindow() if (close is not null) close.Click += (_, _) => Close(); } - public void Configure(PluginService pluginService, IPluginHost host, MainWindowViewModel mainVm) + public void Configure(PluginService pluginService, PluginUIHost host, MainWindowViewModel mainVm) { _pluginService = pluginService; _host = host; _mainVm = mainVm; - _viewModel.Refresh(pluginService.AllPlugins, mainVm.ActivePlugin); + _viewModel.Refresh(pluginService.AllPlugins, host.ActivePluginId); } private async void OnInstall(object? sender, Avalonia.Interactivity.RoutedEventArgs e) @@ -59,26 +59,24 @@ private async void OnInstall(object? sender, Avalonia.Interactivity.RoutedEventA var newPlugins = _pluginService.InstallPlugin(path, _host); if (newPlugins.Count > 0) { - _mainVm.PanelPlugins = _pluginService.PanelPlugins; - _mainVm.TransformPlugins = _pluginService.TransformPlugins; - _viewModel.Refresh(_pluginService.AllPlugins, _mainVm.ActivePlugin); + _mainVm.AllPlugins = _pluginService.AllPlugins; + _viewModel.Refresh(_pluginService.AllPlugins, _host.ActivePluginId); } } private void OnRemove(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { - if (_pluginService is null || _mainVm is null) return; + if (_pluginService is null || _host is null || _mainVm is null) return; var entry = _viewModel.SelectedPlugin; if (entry is null) return; // Deactivate if this is the active panel plugin - if (entry.Plugin is IPanelPlugin panelPlugin && _mainVm.ActivePlugin?.Id == entry.Id) - _mainVm.TogglePluginPanel(panelPlugin); + if (_host.ActivePluginId == entry.Id) + _host.TogglePanel(entry.Plugin); _pluginService.UninstallPlugin(entry.Plugin); - _mainVm.PanelPlugins = _pluginService.PanelPlugins; - _mainVm.TransformPlugins = _pluginService.TransformPlugins; - _viewModel.Refresh(_pluginService.AllPlugins, _mainVm.ActivePlugin); + _mainVm.AllPlugins = _pluginService.AllPlugins; + _viewModel.Refresh(_pluginService.AllPlugins, _host.ActivePluginId); } } diff --git a/src/SharpFM/Services/PluginHost.cs b/src/SharpFM/Services/PluginHost.cs index e2ec59b..b7329b7 100644 --- a/src/SharpFM/Services/PluginHost.cs +++ b/src/SharpFM/Services/PluginHost.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Avalonia.Threading; using Microsoft.Extensions.Logging; using SharpFM.Model; @@ -21,6 +22,14 @@ public class PluginHost : IPluginHost private readonly MainWindowViewModel _viewModel; private readonly ILoggerFactory _loggerFactory; private ClipViewModel? _trackedClip; + private readonly List _repositories = []; + private readonly List _transforms = []; + + /// Repositories registered by plugins. + public IReadOnlyList Repositories => _repositories; + + /// Transforms registered by plugins. + public IReadOnlyList Transforms => _transforms; public PluginHost(MainWindowViewModel viewModel, ILoggerFactory loggerFactory) { @@ -141,6 +150,23 @@ public bool RemoveClip(string clipName) => return true; }); + public void RegisterRepository(IClipRepository repository) => _repositories.Add(repository); + public void RegisterTransform(IClipTransform transform) => _transforms.Add(transform); + + public Task ShowDialogAsync(string title, string message, string[] buttons) + { + // TODO: implement with Avalonia dialog + ShowStatus(message); + return Task.FromResult(null); + } + + public Task ShowInputDialogAsync(string title, string prompt, string? defaultValue = null) + { + // TODO: implement with Avalonia dialog + ShowStatus(prompt); + return Task.FromResult(defaultValue); + } + private ClipViewModel? FindClipByName(string clipName) => _viewModel.FileMakerClips.FirstOrDefault(c => c.Clip.Name.Equals(clipName, StringComparison.OrdinalIgnoreCase)); diff --git a/src/SharpFM/Services/PluginService.cs b/src/SharpFM/Services/PluginService.cs index 4e56a3a..e5f6fb3 100644 --- a/src/SharpFM/Services/PluginService.cs +++ b/src/SharpFM/Services/PluginService.cs @@ -11,34 +11,14 @@ namespace SharpFM.Services; /// /// Discovers, loads, and manages plugin implementations from the plugins/ directory. -/// Supports all plugin types: panel, event, persistence, and transform. /// public class PluginService { private readonly ILogger _logger; - private readonly List _panelPlugins = []; - private readonly List _eventPlugins = []; - private readonly List _persistencePlugins = []; - private readonly List _transformPlugins = []; + private readonly List _plugins = []; - /// Panel plugins that provide sidebar UI. - public IReadOnlyList PanelPlugins => _panelPlugins; - - /// Headless event handler plugins. - public IReadOnlyList EventPlugins => _eventPlugins; - - /// Storage backend plugins. - public IReadOnlyList PersistencePlugins => _persistencePlugins; - - /// Clip transform plugins for import/export pipeline. - public IReadOnlyList TransformPlugins => _transformPlugins; - - /// All loaded plugins across all types. - public IReadOnlyList AllPlugins => - [.. _panelPlugins, .. _eventPlugins, .. _persistencePlugins, .. _transformPlugins]; - - /// Backwards compat: returns panel plugins only. - public IReadOnlyList LoadedPlugins => _panelPlugins; + /// All loaded plugins. + public IReadOnlyList AllPlugins => _plugins; public string PluginsDirectory { get; } @@ -92,10 +72,7 @@ public void LoadPlugins(IPluginHost host) } } - _logger.LogInformation( - "Loaded {Count} plugin(s): {Panel} panel, {Event} event, {Persistence} persistence, {Transform} transform.", - AllPlugins.Count, _panelPlugins.Count, _eventPlugins.Count, - _persistencePlugins.Count, _transformPlugins.Count); + _logger.LogInformation("Loaded {Count} plugin(s).", _plugins.Count); } /// @@ -116,7 +93,7 @@ public IReadOnlyList InstallPlugin(string sourceDllPath, IPluginHost ho File.Copy(sourceDllPath, destPath, overwrite: true); _logger.LogInformation("Copied plugin to {Path}.", destPath); - var beforeCount = AllPlugins.Count; + var beforeCount = _plugins.Count; try { LoadPluginAssembly(destPath, host); @@ -127,7 +104,7 @@ public IReadOnlyList InstallPlugin(string sourceDllPath, IPluginHost ho return []; } - return AllPlugins.Skip(beforeCount).ToList(); + return _plugins.Skip(beforeCount).ToList(); } /// @@ -146,7 +123,7 @@ public bool UninstallPlugin(IPlugin plugin) try { plugin.Dispose(); - RemoveFromTypedList(plugin); + _plugins.Remove(plugin); File.Delete(dllPath); _logger.LogInformation("Uninstalled plugin {Id} from {Path}.", plugin.Id, dllPath); return true; @@ -182,17 +159,6 @@ public IReadOnlyList GetInstalledPluginFiles() return File.Exists(candidate) ? candidate : null; } - private void RemoveFromTypedList(IPlugin plugin) - { - switch (plugin) - { - case IPanelPlugin p: _panelPlugins.Remove(p); break; - case IEventPlugin p: _eventPlugins.Remove(p); break; - case IPersistencePlugin p: _persistencePlugins.Remove(p); break; - case IClipTransformPlugin p: _transformPlugins.Remove(p); break; - } - } - private void LoadPluginAssembly(string dllPath, IPluginHost host) { var fullPath = Path.GetFullPath(dllPath); @@ -221,29 +187,9 @@ private void LoadPluginAssembly(string dllPath, IPluginHost host) if (Activator.CreateInstance(type) is not IPlugin plugin) continue; plugin.Initialize(host); + _plugins.Add(plugin); - switch (plugin) - { - case IPanelPlugin p: - _panelPlugins.Add(p); - break; - case IEventPlugin p: - _eventPlugins.Add(p); - break; - case IPersistencePlugin p: - _persistencePlugins.Add(p); - break; - case IClipTransformPlugin p: - _transformPlugins.Add(p); - break; - default: - _logger.LogWarning("Plugin {Id} implements IPlugin but no known subtype, skipping.", plugin.Id); - plugin.Dispose(); - continue; - } - - _logger.LogInformation("Loaded {Type} plugin: {Id} ({DisplayName})", - plugin.GetType().BaseType?.Name ?? plugin.GetType().Name, plugin.Id, plugin.DisplayName); + _logger.LogInformation("Loaded plugin: {Id} ({DisplayName})", plugin.Id, plugin.DisplayName); } } } diff --git a/src/SharpFM/Services/PluginUIHost.cs b/src/SharpFM/Services/PluginUIHost.cs new file mode 100644 index 0000000..67c7e0a --- /dev/null +++ b/src/SharpFM/Services/PluginUIHost.cs @@ -0,0 +1,101 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Avalonia.Controls; +using SharpFM.Plugin; +using SharpFM.Plugin.UI; + +namespace SharpFM.Services; + +/// +/// Manages plugin panel state and implements . +/// Owns the active panel, its Control, and visibility — the MainWindowViewModel +/// exposes this via composition for XAML binding. +/// +public class PluginUIHost : IPluginUIHost, INotifyPropertyChanged +{ + private readonly IPluginHost _baseHost; + private IPanelPlugin? _activePlugin; + private Control? _panelControl; + + public event PropertyChangedEventHandler? PropertyChanged; + + public PluginUIHost(IPluginHost baseHost) + { + _baseHost = baseHost; + } + + public bool IsVisible => _activePlugin is not null; + + public Control? PanelControl + { + get => _panelControl; + private set { _panelControl = value; OnPropertyChanged(); } + } + + public string? ActivePluginId => _activePlugin?.Id; + + public void TogglePanel(IPlugin plugin) + { + if (plugin is not IPanelPlugin panelPlugin) return; + + if (_activePlugin?.Id == plugin.Id) + { + _activePlugin = null; + PanelControl = null; + } + else + { + PanelControl = panelPlugin.CreatePanel(); + _activePlugin = panelPlugin; + } + + OnPropertyChanged(nameof(IsVisible)); + OnPropertyChanged(nameof(ActivePluginId)); + } + + public bool HasPanel(IPlugin plugin) => plugin is IPanelPlugin; + + // --- IPluginUIHost --- + + public Task ShowContentDialogAsync(string title, Control content) + { + // TODO: implement with Avalonia Window + return Task.FromResult(false); + } + + // --- IPluginHost delegation --- + // All base host methods delegate to the wrapped IPluginHost. + + public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) => _baseHost.CreateLogger(categoryName); + public Model.ClipData? SelectedClip => _baseHost.SelectedClip; + public event System.EventHandler? SelectedClipChanged + { + add => _baseHost.SelectedClipChanged += value; + remove => _baseHost.SelectedClipChanged -= value; + } + public void UpdateSelectedClipXml(string xml, string originPluginId) => _baseHost.UpdateSelectedClipXml(xml, originPluginId); + public event System.EventHandler? ClipContentChanged + { + add => _baseHost.ClipContentChanged += value; + remove => _baseHost.ClipContentChanged -= value; + } + public System.Collections.Generic.IReadOnlyList AllClips => _baseHost.AllClips; + public event System.EventHandler? ClipCollectionChanged + { + add => _baseHost.ClipCollectionChanged += value; + remove => _baseHost.ClipCollectionChanged -= value; + } + public void ShowStatus(string message) => _baseHost.ShowStatus(message); + public Model.ClipData? GetClip(string clipName) => _baseHost.GetClip(clipName); + public void UpdateClipXml(string clipName, string xml, string originPluginId) => _baseHost.UpdateClipXml(clipName, xml, originPluginId); + public void CreateClip(string name, string clipType, string? xml = null) => _baseHost.CreateClip(name, clipType, xml); + public bool RemoveClip(string clipName) => _baseHost.RemoveClip(clipName); + public void RegisterRepository(Model.IClipRepository repository) => _baseHost.RegisterRepository(repository); + public void RegisterTransform(Plugin.IClipTransform transform) => _baseHost.RegisterTransform(transform); + public Task ShowDialogAsync(string title, string message, string[] buttons) => _baseHost.ShowDialogAsync(title, message, buttons); + public Task ShowInputDialogAsync(string title, string prompt, string? defaultValue = null) => _baseHost.ShowInputDialogAsync(title, prompt, defaultValue); + + private void OnPropertyChanged([CallerMemberName] string? name = null) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); +} diff --git a/src/SharpFM/SharpFM.csproj b/src/SharpFM/SharpFM.csproj index c39659f..ad92a20 100644 --- a/src/SharpFM/SharpFM.csproj +++ b/src/SharpFM/SharpFM.csproj @@ -17,6 +17,7 @@ + false diff --git a/src/SharpFM/ViewModels/MainWindowViewModel.cs b/src/SharpFM/ViewModels/MainWindowViewModel.cs index fa3aa6b..8a6486c 100644 --- a/src/SharpFM/ViewModels/MainWindowViewModel.cs +++ b/src/SharpFM/ViewModels/MainWindowViewModel.cs @@ -444,50 +444,17 @@ public string StatusMessage // --- Plugin support --- - private IReadOnlyList _panelPlugins = []; - public IReadOnlyList PanelPlugins + private IReadOnlyList _allPlugins = []; + public IReadOnlyList AllPlugins { - get => _panelPlugins; + get => _allPlugins; set { - _panelPlugins = value; + _allPlugins = value; NotifyPropertyChanged(); } } - private IPanelPlugin? _activePlugin; - public IPanelPlugin? ActivePlugin - { - get => _activePlugin; - private set - { - _activePlugin = value; - NotifyPropertyChanged(); - NotifyPropertyChanged(nameof(IsPluginPanelVisible)); - NotifyPropertyChanged(nameof(PluginPanelControl)); - } - } - - public bool IsPluginPanelVisible => _activePlugin is not null; - - private Control? _pluginPanelControl; - public Control? PluginPanelControl - { - get => _pluginPanelControl; - private set - { - _pluginPanelControl = value; - NotifyPropertyChanged(); - } - } - - private IReadOnlyList _transformPlugins = []; - public IReadOnlyList TransformPlugins - { - get => _transformPlugins; - set { _transformPlugins = value; NotifyPropertyChanged(); } - } - private IReadOnlyList _availableRepositories = []; public IReadOnlyList AvailableRepositories { @@ -495,17 +462,11 @@ public IReadOnlyList AvailableRepositories set { _availableRepositories = value; NotifyPropertyChanged(); } } - public void TogglePluginPanel(IPanelPlugin plugin) + // Panel management is delegated to PluginUIHost (bound via PluginUI property) + private PluginUIHost? _pluginUI; + public PluginUIHost? PluginUI { - if (_activePlugin?.Id == plugin.Id) - { - ActivePlugin = null; - PluginPanelControl = null; - } - else - { - PluginPanelControl = plugin.CreatePanel(); - ActivePlugin = plugin; - } + get => _pluginUI; + set { _pluginUI = value; NotifyPropertyChanged(); } } } diff --git a/tests/SharpFM.Plugin.Tests/PluginInterfaceTests.cs b/tests/SharpFM.Plugin.Tests/PluginInterfaceTests.cs index 8b6377a..0f2cdcf 100644 --- a/tests/SharpFM.Plugin.Tests/PluginInterfaceTests.cs +++ b/tests/SharpFM.Plugin.Tests/PluginInterfaceTests.cs @@ -1,5 +1,6 @@ using SharpFM.Model; using SharpFM.Plugin; +using SharpFM.Plugin.UI; using Xunit; namespace SharpFM.Plugin.Tests; @@ -15,21 +16,9 @@ public void IPanelPlugin_Extends_IPlugin() } [Fact] - public void IEventPlugin_Extends_IPlugin() + public void IPlugin_Extends_IDisposable() { - Assert.True(typeof(IPlugin).IsAssignableFrom(typeof(IEventPlugin))); - } - - [Fact] - public void IPersistencePlugin_Extends_IPlugin() - { - Assert.True(typeof(IPlugin).IsAssignableFrom(typeof(IPersistencePlugin))); - } - - [Fact] - public void IClipTransformPlugin_Extends_IPlugin() - { - Assert.True(typeof(IPlugin).IsAssignableFrom(typeof(IClipTransformPlugin))); + Assert.True(typeof(IDisposable).IsAssignableFrom(typeof(IPlugin))); } [Fact] @@ -38,12 +27,6 @@ public void IPanelPlugin_Extends_IDisposable() Assert.True(typeof(IDisposable).IsAssignableFrom(typeof(IPanelPlugin))); } - [Fact] - public void IEventPlugin_Extends_IDisposable() - { - Assert.True(typeof(IDisposable).IsAssignableFrom(typeof(IEventPlugin))); - } - // --- ClipData record --- [Fact] diff --git a/tests/SharpFM.Plugin.Tests/PluginManagerViewModelTests.cs b/tests/SharpFM.Plugin.Tests/PluginManagerViewModelTests.cs index 3b69f17..8d0e02c 100644 --- a/tests/SharpFM.Plugin.Tests/PluginManagerViewModelTests.cs +++ b/tests/SharpFM.Plugin.Tests/PluginManagerViewModelTests.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using SharpFM.Plugin; +using SharpFM.Plugin.UI; using SharpFM.PluginManager; using Xunit; @@ -11,6 +12,7 @@ private class StubPlugin : IPanelPlugin { public string Id { get; set; } = "stub"; public string DisplayName { get; set; } = "Stub"; + public string Description => ""; public string Version => "1.0.0-test"; public IReadOnlyList KeyBindings => []; public IReadOnlyList MenuActions => []; @@ -25,7 +27,7 @@ public void Refresh_PopulatesPlugins() var vm = new PluginManagerViewModel(); var plugins = new List { new StubPlugin() }; - vm.Refresh(plugins, activePlugin: null); + vm.Refresh(plugins, activePluginId: null); Assert.Single(vm.Plugins); Assert.Equal("stub", vm.Plugins[0].Id); @@ -39,7 +41,7 @@ public void Refresh_MarksActivePlugin() var plugin = new StubPlugin { Id = "active-one" }; var plugins = new List { plugin, new StubPlugin { Id = "other" } }; - vm.Refresh(plugins, activePlugin: plugin); + vm.Refresh(plugins, activePluginId: "active-one"); Assert.True(vm.Plugins[0].IsActive); Assert.False(vm.Plugins[1].IsActive); @@ -57,10 +59,10 @@ public void Refresh_ClearsPreviousEntries() } [Fact] - public void PluginEntry_PluginType_Panel() + public void PluginEntry_Description() { var entry = new PluginEntry(new StubPlugin(), false); - Assert.Equal("Panel", entry.PluginType); + Assert.Equal("", entry.Description); } [Fact] diff --git a/tests/SharpFM.Plugin.Tests/PluginServiceTests.cs b/tests/SharpFM.Plugin.Tests/PluginServiceTests.cs index 1c4a24d..f8d2455 100644 --- a/tests/SharpFM.Plugin.Tests/PluginServiceTests.cs +++ b/tests/SharpFM.Plugin.Tests/PluginServiceTests.cs @@ -1,9 +1,8 @@ using System.IO; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using SharpFM.Model; -using SharpFM.Model.Schema; -using SharpFM.Model.Scripting; using SharpFM.Plugin; using SharpFM.Services; using Xunit; @@ -19,15 +18,15 @@ public class MockPluginHost : IPluginHost public event EventHandler? ClipCollectionChanged; public ILogger CreateLogger(string categoryName) => NullLogger.Instance; public ClipData? GetClip(string clipName) => AllClips.FirstOrDefault(c => c.Name.Equals(clipName, StringComparison.OrdinalIgnoreCase)); - public IReadOnlyList? GetScriptSteps(string clipName) => null; - public IReadOnlyList UpdateScriptSteps(string clipName, IReadOnlyList operations, string originPluginId) => []; - public IReadOnlyList? GetTableFields(string clipName) => null; - public IReadOnlyList UpdateTableFields(string clipName, IReadOnlyList operations, string originPluginId) => []; public void UpdateClipXml(string clipName, string xml, string originPluginId) { } public void CreateClip(string name, string clipType, string? xml = null) { } public bool RemoveClip(string clipName) => false; public void UpdateSelectedClipXml(string xml, string originPluginId) { } public void ShowStatus(string message) { LastStatus = message; } + public void RegisterRepository(IClipRepository repository) { } + public void RegisterTransform(IClipTransform transform) { } + public Task ShowDialogAsync(string title, string message, string[] buttons) => Task.FromResult(null); + public Task ShowInputDialogAsync(string title, string prompt, string? defaultValue = null) => Task.FromResult(null); public string? LastStatus { get; private set; } public void RaiseChanged(ClipData? clip) => SelectedClipChanged?.Invoke(this, clip); public void RaiseContentChanged(ClipContentChangedArgs args) => ClipContentChanged?.Invoke(this, args); @@ -51,7 +50,7 @@ public void LoadPlugins_NoPluginsDir_LoadsZero() // AppContext.BaseDirectory won't have a plugins/ dir in test service.LoadPlugins(host); - Assert.Empty(service.LoadedPlugins); + Assert.Empty(service.AllPlugins); } [Fact] @@ -64,7 +63,7 @@ public void LoadPlugins_EmptyDir_LoadsZero() { var service = CreateService(dir); service.LoadPlugins(new MockPluginHost()); - Assert.Empty(service.LoadedPlugins); + Assert.Empty(service.AllPlugins); } finally { @@ -85,7 +84,7 @@ public void LoadPlugins_InvalidDll_GracefullySkips() var service = CreateService(dir); service.LoadPlugins(new MockPluginHost()); - Assert.Empty(service.LoadedPlugins); + Assert.Empty(service.AllPlugins); } finally { @@ -119,23 +118,10 @@ public void InstallPlugin_InvalidDll_CopiesButReturnsEmpty() } [Fact] - public void AllPlugins_AggregatesAllTypes() + public void AllPlugins_DefaultsToEmpty() { var service = CreateService("/tmp/nonexistent-" + Guid.NewGuid()); - // No plugins loaded, but verify the property returns empty aggregate Assert.Empty(service.AllPlugins); - Assert.Empty(service.PanelPlugins); - Assert.Empty(service.EventPlugins); - Assert.Empty(service.PersistencePlugins); - Assert.Empty(service.TransformPlugins); - } - - [Fact] - public void LoadedPlugins_ReturnsPanelPlugins() - { - var service = CreateService("/tmp/nonexistent-" + Guid.NewGuid()); - // LoadedPlugins is a backwards-compat alias for PanelPlugins - Assert.Same(service.PanelPlugins, service.LoadedPlugins); } [Fact] diff --git a/tests/SharpFM.Plugin.Tests/SharpFM.Plugin.Tests.csproj b/tests/SharpFM.Plugin.Tests/SharpFM.Plugin.Tests.csproj index 98b8fe1..e25ba8a 100644 --- a/tests/SharpFM.Plugin.Tests/SharpFM.Plugin.Tests.csproj +++ b/tests/SharpFM.Plugin.Tests/SharpFM.Plugin.Tests.csproj @@ -26,6 +26,7 @@ + diff --git a/tests/SharpFM.Plugin.Tests/XmlViewerPluginTests.cs b/tests/SharpFM.Plugin.Tests/XmlViewerPluginTests.cs index 6d62f9d..c411be3 100644 --- a/tests/SharpFM.Plugin.Tests/XmlViewerPluginTests.cs +++ b/tests/SharpFM.Plugin.Tests/XmlViewerPluginTests.cs @@ -1,6 +1,5 @@ +using System.Threading.Tasks; using SharpFM.Model; -using SharpFM.Model.Schema; -using SharpFM.Model.Scripting; using SharpFM.Plugin; using SharpFM.Plugin.XmlViewer; using Xunit; @@ -151,14 +150,14 @@ public void UpdateSelectedClipXml(string xml, string originPluginId) public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) => Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; public ClipData? GetClip(string clipName) => AllClips.FirstOrDefault(c => c.Name.Equals(clipName, StringComparison.OrdinalIgnoreCase)); - public IReadOnlyList? GetScriptSteps(string clipName) => null; - public IReadOnlyList UpdateScriptSteps(string clipName, IReadOnlyList operations, string originPluginId) => []; - public IReadOnlyList? GetTableFields(string clipName) => null; - public IReadOnlyList UpdateTableFields(string clipName, IReadOnlyList operations, string originPluginId) => []; public void CreateClip(string name, string clipType, string? xml = null) { } public bool RemoveClip(string clipName) => false; public void UpdateClipXml(string clipName, string xml, string originPluginId) { LastUpdatedXml = xml; LastOriginPluginId = originPluginId; } public void ShowStatus(string message) { } + public void RegisterRepository(IClipRepository repository) { } + public void RegisterTransform(IClipTransform transform) { } + public Task ShowDialogAsync(string title, string message, string[] buttons) => Task.FromResult(null); + public Task ShowInputDialogAsync(string title, string prompt, string? defaultValue = null) => Task.FromResult(null); public void RaiseChanged(ClipData? clip) => SelectedClipChanged?.Invoke(this, clip); public void RaiseContentChanged(ClipContentChangedArgs args) => ClipContentChanged?.Invoke(this, args); } diff --git a/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs b/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs index 9bb531e..cbe74ad 100644 --- a/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs +++ b/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using SharpFM.Plugin; +using SharpFM.Plugin.UI; using SharpFM.Services; using SharpFM.ViewModels; using Xunit; @@ -173,62 +174,62 @@ public void SearchText_FiltersClips() } [Fact] - public void PanelPlugins_DefaultsToEmpty() + public void AllPlugins_DefaultsToEmpty() { var vm = CreateVm(); - Assert.Empty(vm.PanelPlugins); + Assert.Empty(vm.AllPlugins); } [Fact] - public void PanelPlugins_CanBeSet() + public void PluginUI_TogglePanel_ActivatesPlugin() { var vm = CreateVm(); var plugin = new StubPanelPlugin(); - vm.PanelPlugins = [plugin]; - Assert.Single(vm.PanelPlugins); - } + vm.AllPlugins = [plugin]; + var host = new MockPluginHost(); + var uiHost = new PluginUIHost(host); + vm.PluginUI = uiHost; - [Fact] - public void TogglePluginPanel_ActivatesPlugin() - { - var vm = CreateVm(); - var plugin = new StubPanelPlugin(); - vm.PanelPlugins = [plugin]; + uiHost.TogglePanel(plugin); - vm.TogglePluginPanel(plugin); - - Assert.True(vm.IsPluginPanelVisible); - Assert.Same(plugin, vm.ActivePlugin); - Assert.NotNull(vm.PluginPanelControl); + Assert.True(uiHost.IsVisible); + Assert.Equal(plugin.Id, uiHost.ActivePluginId); + Assert.NotNull(uiHost.PanelControl); } [Fact] - public void TogglePluginPanel_DeactivatesSamePlugin() + public void PluginUI_TogglePanel_DeactivatesSamePlugin() { var vm = CreateVm(); var plugin = new StubPanelPlugin(); - vm.PanelPlugins = [plugin]; + vm.AllPlugins = [plugin]; + var host = new MockPluginHost(); + var uiHost = new PluginUIHost(host); + vm.PluginUI = uiHost; - vm.TogglePluginPanel(plugin); - vm.TogglePluginPanel(plugin); + uiHost.TogglePanel(plugin); + uiHost.TogglePanel(plugin); - Assert.False(vm.IsPluginPanelVisible); - Assert.Null(vm.ActivePlugin); + Assert.False(uiHost.IsVisible); + Assert.Null(uiHost.ActivePluginId); } [Fact] - public void TogglePluginPanel_SwitchesPlugins() + public void PluginUI_TogglePanel_SwitchesPlugins() { var vm = CreateVm(); var plugin1 = new StubPanelPlugin { Id = "p1" }; var plugin2 = new StubPanelPlugin { Id = "p2" }; - vm.PanelPlugins = [plugin1, plugin2]; + vm.AllPlugins = [plugin1, plugin2]; + var host = new MockPluginHost(); + var uiHost = new PluginUIHost(host); + vm.PluginUI = uiHost; - vm.TogglePluginPanel(plugin1); - Assert.Same(plugin1, vm.ActivePlugin); + uiHost.TogglePanel(plugin1); + Assert.Equal("p1", uiHost.ActivePluginId); - vm.TogglePluginPanel(plugin2); - Assert.Same(plugin2, vm.ActivePlugin); + uiHost.TogglePanel(plugin2); + Assert.Equal("p2", uiHost.ActivePluginId); } [Fact] @@ -243,10 +244,31 @@ public void PluginWithKeyBindings_ExposesBindings() Assert.True(called); } + private class MockPluginHost : IPluginHost + { + public Model.ClipData? SelectedClip { get; set; } + public IReadOnlyList AllClips { get; set; } = []; + public event EventHandler? SelectedClipChanged; + public event EventHandler? ClipContentChanged; + public event EventHandler? ClipCollectionChanged; + public ILogger CreateLogger(string categoryName) => NullLogger.Instance; + public Model.ClipData? GetClip(string clipName) => null; + public void UpdateClipXml(string clipName, string xml, string originPluginId) { } + public void CreateClip(string name, string clipType, string? xml = null) { } + public bool RemoveClip(string clipName) => false; + public void UpdateSelectedClipXml(string xml, string originPluginId) { } + public void ShowStatus(string message) { } + public void RegisterRepository(Model.IClipRepository repository) { } + public void RegisterTransform(IClipTransform transform) { } + public Task ShowDialogAsync(string title, string message, string[] buttons) => Task.FromResult(null); + public Task ShowInputDialogAsync(string title, string prompt, string? defaultValue = null) => Task.FromResult(null); + } + private class StubPanelPlugin : IPanelPlugin { public string Id { get; set; } = "stub"; public string DisplayName => "Stub Plugin"; + public string Description => ""; public string Version => "1.0.0-test"; public IReadOnlyList TestKeyBindings { get; set; } = []; public IReadOnlyList KeyBindings => TestKeyBindings;