diff --git a/src/modules/cmdpal/Exts/EverythingExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/EverythingExtension/SampleExtension.cs index 7895a1944f..9e191415ba 100644 --- a/src/modules/cmdpal/Exts/EverythingExtension/SampleExtension.cs +++ b/src/modules/cmdpal/Exts/EverythingExtension/SampleExtension.cs @@ -12,10 +12,12 @@ namespace EverythingExtension; [ComVisible(true)] [Guid("c4d344ce-480a-4ef5-9875-96e7bf2b6992")] [ComDefaultInterface(typeof(IExtension))] -public sealed partial class SampleExtension : IExtension +public sealed partial class SampleExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; + private readonly EverythingExtensionActionsProvider _provider = new(); + public SampleExtension(ManualResetEvent extensionDisposedEvent) { this._extensionDisposedEvent = extensionDisposedEvent; @@ -26,7 +28,7 @@ public sealed partial class SampleExtension : IExtension switch (providerType) { case ProviderType.Commands: - return new EverythingExtensionActionsProvider(); + return _provider; default: return null; } diff --git a/src/modules/cmdpal/Exts/HackerNewsExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/HackerNewsExtension/SampleExtension.cs index 1e521a0160..8e7d657e4c 100644 --- a/src/modules/cmdpal/Exts/HackerNewsExtension/SampleExtension.cs +++ b/src/modules/cmdpal/Exts/HackerNewsExtension/SampleExtension.cs @@ -12,10 +12,12 @@ namespace HackerNewsExtension; [ComVisible(true)] [Guid("283DDB0F-1AD9-406F-B359-699BFBD2DA68")] [ComDefaultInterface(typeof(IExtension))] -public sealed partial class SampleExtension : IExtension +public sealed partial class SampleExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; + private readonly HackerNewsCommandsProvider _provider = new(); + public SampleExtension(ManualResetEvent extensionDisposedEvent) { this._extensionDisposedEvent = extensionDisposedEvent; @@ -26,7 +28,7 @@ public sealed partial class SampleExtension : IExtension switch (providerType) { case ProviderType.Commands: - return new HackerNewsCommandsProvider(); + return _provider; default: return null; } diff --git a/src/modules/cmdpal/Exts/MastodonExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/MastodonExtension/SampleExtension.cs index 79d903ccb2..2eae6e72a6 100644 --- a/src/modules/cmdpal/Exts/MastodonExtension/SampleExtension.cs +++ b/src/modules/cmdpal/Exts/MastodonExtension/SampleExtension.cs @@ -12,10 +12,12 @@ namespace MastodonExtension; [ComVisible(true)] [Guid("f0e93f1a-2b64-4896-abcc-8d2145480ede")] [ComDefaultInterface(typeof(IExtension))] -public sealed partial class SampleExtension : IExtension +public sealed partial class SampleExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; + private readonly MastodonExtensionActionsProvider _provider = new(); + public SampleExtension(ManualResetEvent extensionDisposedEvent) { this._extensionDisposedEvent = extensionDisposedEvent; @@ -26,7 +28,7 @@ public sealed partial class SampleExtension : IExtension switch (providerType) { case ProviderType.Commands: - return new MastodonExtensionActionsProvider(); + return _provider; default: return null; } diff --git a/src/modules/cmdpal/Exts/MediaControlsExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/MediaControlsExtension/SampleExtension.cs index 5d417c00a0..ab8bc4e7a4 100644 --- a/src/modules/cmdpal/Exts/MediaControlsExtension/SampleExtension.cs +++ b/src/modules/cmdpal/Exts/MediaControlsExtension/SampleExtension.cs @@ -12,10 +12,12 @@ namespace MediaControlsExtension; [ComVisible(true)] [Guid("bb60a98a-0197-4378-9b40-b684f4068d1d")] [ComDefaultInterface(typeof(IExtension))] -public sealed partial class SampleExtension : IExtension +public sealed partial class SampleExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; + private readonly MediaActionsProvider _provider = new(); + public SampleExtension(ManualResetEvent extensionDisposedEvent) { this._extensionDisposedEvent = extensionDisposedEvent; @@ -26,7 +28,7 @@ public sealed partial class SampleExtension : IExtension switch (providerType) { case ProviderType.Commands: - return new MediaActionsProvider(); + return _provider; default: return null; } diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/ProcessMonitorExtension/SampleExtension.cs index 0e593068ef..3fd1cc6921 100644 --- a/src/modules/cmdpal/Exts/ProcessMonitorExtension/SampleExtension.cs +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/SampleExtension.cs @@ -12,10 +12,12 @@ namespace ProcessMonitorExtension; [ComVisible(true)] [Guid("8BD7A6C4-7185-4426-AE8D-61E438A3E740")] [ComDefaultInterface(typeof(IExtension))] -public sealed partial class SampleExtension : IExtension +public sealed partial class SampleExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; + private readonly ProcessMonitorCommandProvider _provider = new(); + public SampleExtension(ManualResetEvent extensionDisposedEvent) { this._extensionDisposedEvent = extensionDisposedEvent; @@ -26,7 +28,7 @@ public sealed partial class SampleExtension : IExtension switch (providerType) { case ProviderType.Commands: - return new ProcessMonitorCommandProvider(); + return _provider; default: return null; } diff --git a/src/modules/cmdpal/Exts/SSHKeychainExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/SSHKeychainExtension/SampleExtension.cs index f0c72184f2..7f8836dfde 100644 --- a/src/modules/cmdpal/Exts/SSHKeychainExtension/SampleExtension.cs +++ b/src/modules/cmdpal/Exts/SSHKeychainExtension/SampleExtension.cs @@ -12,10 +12,12 @@ namespace SSHKeychainExtension; [ComVisible(true)] [Guid("D07A5785-2334-4686-9A49-AE19D992284F")] [ComDefaultInterface(typeof(IExtension))] -public sealed partial class SampleExtension : IExtension +public sealed partial class SampleExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; + private readonly SSHKeychainCommandsProvider _provider = new(); + public SampleExtension(ManualResetEvent extensionDisposedEvent) { this._extensionDisposedEvent = extensionDisposedEvent; @@ -26,7 +28,7 @@ public sealed partial class SampleExtension : IExtension switch (providerType) { case ProviderType.Commands: - return new SSHKeychainCommandsProvider(); + return _provider; default: return null; } diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleSettingsPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleSettingsPage.cs new file mode 100644 index 0000000000..dfa459ea3b --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleSettingsPage.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace SamplePagesExtension; + +internal sealed partial class SampleSettingsPage : FormPage +{ + private readonly Settings _settings = new(); + + public override IForm[] Forms() + { + var s = _settings.ToForms(); + return s; + } + + public SampleSettingsPage() + { + Name = "Sample Settings"; + Icon = new(string.Empty); + _settings.Add(new ToggleSetting("onOff", true) + { + Label = "This is a toggle", + Description = "It produces a simple checkbox", + }); + _settings.Add(new TextSetting("someText", "initial value") + { + Label = "This is a text box", + Description = "For some string of text", + }); + + _settings.SettingsChanged += SettingsChanged; + } + + private void SettingsChanged(object sender, Settings args) + { + /* Do something with the new settings here */ + var onOff = _settings.GetSetting("onOff"); + ExtensionHost.LogMessage(new LogMessage() { Message = $"SampleSettingsPage: Changed the value of onOff to {onOff}" }); + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Properties/launchSettings.json b/src/modules/cmdpal/Exts/SamplePagesExtension/Properties/launchSettings.json index 36ae993894..d603cc3a70 100644 --- a/src/modules/cmdpal/Exts/SamplePagesExtension/Properties/launchSettings.json +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Properties/launchSettings.json @@ -1,7 +1,8 @@ { "profiles": { "SamplePagesExtension (Package)": { - "commandName": "MsixPackage" + "commandName": "MsixPackage", + "doNotLaunchApp": true }, "SamplePagesExtension (Unpackaged)": { "commandName": "Project" diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/SampleExtension.cs index 76e1a13fe5..20a0b238ea 100644 --- a/src/modules/cmdpal/Exts/SamplePagesExtension/SampleExtension.cs +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/SampleExtension.cs @@ -12,10 +12,12 @@ namespace SamplePagesExtension; [ComVisible(true)] [Guid("6112D28D-6341-45C8-92C3-83ED55853A9F")] [ComDefaultInterface(typeof(IExtension))] -public sealed partial class SampleExtension : IExtension +public sealed partial class SampleExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; + private readonly SamplePagesCommandsProvider _provider = new(); + public SampleExtension(ManualResetEvent extensionDisposedEvent) { this._extensionDisposedEvent = extensionDisposedEvent; @@ -26,7 +28,7 @@ public sealed partial class SampleExtension : IExtension switch (providerType) { case ProviderType.Commands: - return new SamplePagesCommandsProvider(); + return _provider; default: return null; } diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesCommandsProvider.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesCommandsProvider.cs index ea7c34ee68..192ff456f2 100644 --- a/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesCommandsProvider.cs +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesCommandsProvider.cs @@ -40,6 +40,11 @@ public partial class SamplePagesCommandsProvider : CommandProvider { Title = "Dynamic List Page Command", Subtitle = "SamplePages Extension", + }, + new ListItem(new SampleSettingsPage()) + { + Title = "Sample settings page", + Subtitle = "A demo of the settings helpers", } ]; diff --git a/src/modules/cmdpal/Exts/SpongebotExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/SpongebotExtension/SampleExtension.cs index bfe9a9cf4e..c898b2a5c1 100644 --- a/src/modules/cmdpal/Exts/SpongebotExtension/SampleExtension.cs +++ b/src/modules/cmdpal/Exts/SpongebotExtension/SampleExtension.cs @@ -12,10 +12,12 @@ namespace SpongebotExtension; [ComVisible(true)] [Guid("a50859fc-a214-4852-b47b-62ada70df7bc")] [ComDefaultInterface(typeof(IExtension))] -public sealed partial class SampleExtension : IExtension +public sealed partial class SampleExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; + private readonly SpongebotCommandsProvider _provider = new(); + public SampleExtension(ManualResetEvent extensionDisposedEvent) { this._extensionDisposedEvent = extensionDisposedEvent; @@ -26,7 +28,7 @@ public sealed partial class SampleExtension : IExtension switch (providerType) { case ProviderType.Commands: - return new SpongebotCommandsProvider(); + return _provider; default: return null; } diff --git a/src/modules/cmdpal/Exts/TemplateExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/TemplateExtension/SampleExtension.cs index 7d28e2581a..5c09bbfb2f 100644 --- a/src/modules/cmdpal/Exts/TemplateExtension/SampleExtension.cs +++ b/src/modules/cmdpal/Exts/TemplateExtension/SampleExtension.cs @@ -12,10 +12,12 @@ namespace TemplateExtension; [ComVisible(true)] [Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")] [ComDefaultInterface(typeof(IExtension))] -public sealed partial class SampleExtension : IExtension +public sealed partial class SampleExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; + private readonly TemplateExtensionActionsProvider _provider = new(); + public SampleExtension(ManualResetEvent extensionDisposedEvent) { this._extensionDisposedEvent = extensionDisposedEvent; @@ -26,7 +28,7 @@ public sealed partial class SampleExtension : IExtension switch (providerType) { case ProviderType.Commands: - return new TemplateExtensionActionsProvider(); + return _provider; default: return null; } diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/YouTubeExtension/SampleExtension.cs index e4e21e341d..21187b55a6 100644 --- a/src/modules/cmdpal/Exts/YouTubeExtension/SampleExtension.cs +++ b/src/modules/cmdpal/Exts/YouTubeExtension/SampleExtension.cs @@ -12,10 +12,12 @@ namespace YouTubeExtension; [ComVisible(true)] [Guid("95696eff-5c44-41ad-8f64-c2182ec9e3fd")] [ComDefaultInterface(typeof(IExtension))] -public sealed partial class SampleExtension : IExtension +public sealed partial class SampleExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; + private readonly YouTubeExtensionActionsProvider _provider = new(); + public SampleExtension(ManualResetEvent extensionDisposedEvent) { this._extensionDisposedEvent = extensionDisposedEvent; @@ -26,7 +28,7 @@ public sealed partial class SampleExtension : IExtension switch (providerType) { case ProviderType.Commands: - return new YouTubeExtensionActionsProvider(); + return _provider; default: return null; } diff --git a/src/modules/cmdpal/WindowsCommandPalette/Views/FormViewModel.xaml.cs b/src/modules/cmdpal/WindowsCommandPalette/Views/FormViewModel.xaml.cs index fe60b40c3f..019f8e3bba 100644 --- a/src/modules/cmdpal/WindowsCommandPalette/Views/FormViewModel.xaml.cs +++ b/src/modules/cmdpal/WindowsCommandPalette/Views/FormViewModel.xaml.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.ComponentModel; +using System.Diagnostics; using System.Text.Json; using AdaptiveCards.ObjectModel.WinUI3; using AdaptiveCards.Rendering.WinUI3; @@ -102,6 +103,12 @@ public sealed class FormViewModel : INotifyPropertyChanged var handlers = RequestSubmitForm; handlers?.Invoke(this, new() { FormData = inputs, Form = _form }); } + else if (args.Action is AdaptiveExecuteAction executeAction) + { + var inputs = executeAction.DataJson?.Stringify(); + _ = inputs; + Debug.WriteLine($"Execute form: {inputs}"); + } } private static readonly string ErrorCardJson = """ diff --git a/src/modules/cmdpal/WindowsCommandPalette/Views/MainPage.xaml.cs b/src/modules/cmdpal/WindowsCommandPalette/Views/MainPage.xaml.cs index 1ca862b011..6b97feffa1 100644 --- a/src/modules/cmdpal/WindowsCommandPalette/Views/MainPage.xaml.cs +++ b/src/modules/cmdpal/WindowsCommandPalette/Views/MainPage.xaml.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.CmdPal.Common.Extensions; using Microsoft.CmdPal.Common.Services; @@ -166,8 +167,21 @@ public sealed partial class MainPage : Page { var formData = args.FormData; var form = args.Form; - var result = form.SubmitForm(formData); - HandleResult(result); + + // TODO ~ when we have a real MVVM app ~ + // This should be done in a Task, on a background thread, and awaited + try + { + var result = form.SubmitForm(formData); + + // Log successful form submission + HandleResult(result); + } + catch (Exception e) + { + Debug.WriteLine("Error submitting form to extension"); + Debug.WriteLine(e.Message); + } } private void HandleResult(ICommandResult? res) diff --git a/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md b/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md index 7a54d58b22..d9307dc487 100644 --- a/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md +++ b/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md @@ -1,7 +1,7 @@ --- author: Mike Griese created on: 2024-07-19 -last updated: 2024-11-04 +last updated: 2024-11-07 issue id: n/a --- @@ -62,10 +62,10 @@ functionality. - [`INotifyPropChanged`](#inotifypropchanged) - [`ICommandProvider`](#icommandprovider) - [Settings](#settings) - - [Retrieving info from the host](#retrieving-info-from-the-host) - [Helper SDK Classes](#helper-sdk-classes) - [Default implementations](#default-implementations) - [Using the Clipboard](#using-the-clipboard) + - [Settings helpers](#settings-helpers) - [Advanced scenarios](#advanced-scenarios) - [Status messages](#status-messages) - [Class diagram](#class-diagram) @@ -1118,12 +1118,16 @@ prevent confusion with the XAML version. This is the interface that an extension must implement to provide commands to DevPal. ```csharp +interface ICommandSettings { + IFormPage SettingsPage { get; }; +}; + interface ICommandProvider requires Windows.Foundation.IClosable { String DisplayName { get; }; IconDataType Icon { get; }; + ICommandSettings Settings { get; }; // TODO! Boolean CanBeCached { get; }; - // TODO! IFormPage SettingsPage { get; }; IListItem[] TopLevelCommands(); @@ -1141,21 +1145,20 @@ top-level items are `IListItem`s, they can have `MoreCommands`, `Details` and ### Settings -Extensions may also want to provide settings to the user. +Extensions may also want to provide settings to the user. They can do this by +implementing the `ICommandSettings` interface. This interface has a single +property, `SettingsPage`, which is a `FormPage`. (We're adding the layer of +abstraction here to allow for further additions to `ICommandSettings` in the +future.) -[TODO!]: write this +In the DevPal settings page, we can then link to each extension's given settings +page. As these pages are just `FormPage`s, they can be as simple or as complex +as the extension developer wants, and they're rendered and interacted with in +the same way. -It would be pretty trivial to just allow apps to provide a `FormPage` as their -settings page. That would hilariously just work I think. I dunno if Adaptive -Cards is great for real-time saving of settings though. - -We probably also want to provide a helper class for storing settings, so that -apps don't need to worry too much about mucking around with that. I'm especially -thinking about storing credentials securely. - -### Retrieving info from the host - -TODO! write me +We're then additionally going to provide a collection of settings helpers for +developers in the helper SDK. This should allow developers to quickly work to +add settings, without mucking around in building the form JSON themselves. ## Helper SDK Classes @@ -1244,6 +1247,83 @@ presents persistent difficulties. We'll provide a helper class that allows developers to easily use the clipboard in their extensions. +### Settings helpers + +The DevPal helpers library also includes a set of helpers for building settings +pages for you. This lets you define a `Settings` object as a collection of +properties, controlled by how they're presented in the UI. The helpers library +will then handle the process of turning those properties into a `IForm` for you. + +As a complete example: Here's a sample of an app which defines a pair of +settings (`onOff` and `whatever`) in their `MyAppSettings` class. +`MyAppSettings` can be responsible for loading or saving the settings however +the developer best sees fit. They then pass an instance of that object to the +`MySettingsPage` class they define. In `MySettingsPage.Forms`, the developer +doesn't need to do any work to build up the Adaptive Card JSON at all. Just call +`Settings.ToForms()`. The generated form will call back to the extension's code +in `SettingsChanged` when the user submits the `IForm`. At that point, the +extension author is again free to do whatever they'd like - store the json +wherever they want, use the updated values, whatever. + +```cs +class MyAppSettings { + private readonly Helpers.Settings _settings = new(); + public Helpers.Settings Settings => _settings; + + public MyAppSettings() { + // Define the structure of your settings here. + var onOffSetting = new Helpers.ToggleSetting("onOff", "Enable feature", "This feature will do something cool", true); + var textSetting = new Helpers.TextSetting("whatever", "Text setting", "This is a text setting", "Default text"); + _settings.Add(onOffSetting); + _settings.Add(onOffSetting); + } + public void LoadSavedData() + { + // Possibly, load the settings from a file or something + var persistedData = /* load the settings from file */; + _settings.LoadState(persistedData); + } + public void SaveSettings() + { + /* You can save the settings to the file here */ + var mySettingsFilePath = /* whatever */; + string mySettingsJson = mySettings.Settings.GetState(); + // Or you could raise a event to indicate to the rest of your app that settings have changed. + } +} + +class MySettingsPage : Microsoft.Windows.Run.Extensions.FormPage +{ + private readonly MyAppSettings mySettings; + public MySettingsPage(MyAppSettings s) { + mySettings = s; + mySettings.Settings.SettingsChanged += SettingsChanged; + } + public override IForm[] Forms() { + // If you haven't already: + mySettings.Settings.LoadSavedData(); + return mySettings.Settings.ToForms(); + } + + private void SettingsChanged(object sender, Settings args) + { + /* Do something with the new settings here */ + var onOff = _settings.GetSetting("onOff"); + ExtensionHost.LogMessage(new LogMessage() { Message = $"MySettingsPage: Changed the value of onOff to {onOff}" }); + + // Possibly even: + mySettings.SaveSettings(); + } +} + +// elsewhere in your app: + +MyAppSettings instance = /* Up to you how you want to pass this around. + Singleton, dependency injection, whatever. */ +var onOff = instance.Settings.Get("onOff"); + +``` + ## Advanced scenarios ### Status messages diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/CommandProvider.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/CommandProvider.cs index d212075585..fa9d38fe90 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/CommandProvider.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/CommandProvider.cs @@ -10,12 +10,16 @@ public partial class CommandProvider : ICommandProvider private IconDataType _icon = new(string.Empty); + private ICommandSettings? _settings; + public string DisplayName { get => _displayName; protected set => _displayName = value; } public IconDataType Icon { get => _icon; protected set => _icon = value; } public virtual IListItem[] TopLevelCommands() => throw new NotImplementedException(); + public ICommandSettings? Settings { get => _settings; protected set => _settings = value; } + public void InitializeWithHost(IExtensionHost host) { ExtensionHost.Initialize(host); diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/ISettingsForm.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/ISettingsForm.cs new file mode 100644 index 0000000000..6e3ec4ca0f --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/ISettingsForm.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Nodes; + +namespace Microsoft.CmdPal.Extensions.Helpers; + +internal interface ISettingsForm +{ + public string ToForm(); + + public void Update(JsonObject payload); + + public Dictionary ToDictionary(); + + public string ToDataIdentifier(); +} diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Setting`1.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Setting`1.cs new file mode 100644 index 0000000000..2f62fb71f8 --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Setting`1.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.CmdPal.Extensions.Helpers; + +public abstract class Setting : ISettingsForm +{ + private readonly string _key; + + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true }; + + public T? Value { get; set; } + + public string Key => _key; + + public bool IsRequired { get; set; } + + public string ErrorMessage { get; set; } = string.Empty; + + public string Label { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + protected Setting() + { + Value = default; + _key = string.Empty; + } + + public Setting(string key, T defaultValue) + { + _key = key; + Value = defaultValue; + } + + public Setting(string key, string label, string description, T defaultValue) + { + _key = key; + Value = defaultValue; + Label = label; + Description = description; + } + + public abstract Dictionary ToDictionary(); + + public string ToDataIdentifier() + { + return $"\"{_key}\": \"{_key}\""; + } + + public string ToForm() + { + var bodyJson = JsonSerializer.Serialize(ToDictionary(), _jsonSerializerOptions); + var dataJson = $"\"{_key}\": \"{_key}\""; + + var json = $$""" +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + {{bodyJson}} + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Save", + "data": { + {{dataJson}} + } + } + ] +} +"""; + return json; + } + + public abstract void Update(JsonObject payload); +} diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Settings.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Settings.cs new file mode 100644 index 0000000000..b332159062 --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Settings.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Extensions.Helpers; + +public sealed class Settings +{ + private readonly Dictionary _settings = new(); + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true }; + + public event TypedEventHandler? SettingsChanged; + + public void Add(Setting s) + { + _settings.Add(s.Key, s); + } + + public T? GetSetting(string key) + { + return _settings[key] is Setting s ? s.Value : default; + } + + public bool TryGetSetting(string key, out T? val) + { + object? o; + if (_settings.TryGetValue(key, out o)) + { + if (o is Setting s) + { + val = s.Value; + return true; + } + } + + val = default; + return false; + } + + internal string ToFormJson() + { + var settings = _settings + .Values + .Where(s => s is ISettingsForm) + .Select(s => s as ISettingsForm) + .Where(s => s != null) + .Select(s => s!); + + var bodies = string.Join(",", settings + .Select(s => JsonSerializer.Serialize(s.ToDictionary(), _jsonSerializerOptions))); + var datas = string.Join(",", settings + .Select(s => s.ToDataIdentifier())); + + var json = $$""" +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + {{bodies}} + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Save", + "data": { + {{datas}} + } + } + ] +} +"""; + return json; + } + + public IForm[] ToForms() + { + return [new SettingsForm(this)]; + } + + public void Update(string data) + { + var formInput = JsonNode.Parse(data)?.AsObject(); + if (formInput == null) + { + return; + } + + foreach (var key in _settings.Keys) + { + var value = _settings[key]; + if (value is ISettingsForm f) + { + f.Update(formInput); + } + } + } + + internal void RaiseSettingsChanged() + { + var handlers = SettingsChanged; + handlers?.Invoke(this, this); + } +} diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/SettingsForm.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/SettingsForm.cs new file mode 100644 index 0000000000..36b91c4dbf --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/SettingsForm.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Nodes; + +namespace Microsoft.CmdPal.Extensions.Helpers; + +public partial class SettingsForm : Form +{ + private readonly Settings _settings; + + internal SettingsForm(Settings settings) + { + _settings = settings; + Template = _settings.ToFormJson(); + } + + public override ICommandResult SubmitForm(string payload) + { + var formInput = JsonNode.Parse(payload)?.AsObject(); + if (formInput == null) + { + return CommandResult.KeepOpen(); + } + + _settings.Update(payload); + _settings.RaiseSettingsChanged(); + + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/TextSetting.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/TextSetting.cs new file mode 100644 index 0000000000..9c6c7b3d90 --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/TextSetting.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Nodes; + +namespace Microsoft.CmdPal.Extensions.Helpers; + +public class TextSetting : Setting +{ + private TextSetting() + : base() + { + Value = string.Empty; + } + + public TextSetting(string key, string defaultValue) + : base(key, defaultValue) + { + } + + public TextSetting(string key, string label, string description, string defaultValue) + : base(key, label, description, defaultValue) + { + } + + public override Dictionary ToDictionary() + { + return new Dictionary + { + { "type", "Input.Text" }, + { "title", Label }, + { "id", Key }, + { "label", Description }, + { "value", Value ?? string.Empty }, + { "isRequired", IsRequired }, + { "errorMessage", ErrorMessage }, + }; + } + + public static TextSetting LoadFromJson(JsonObject jsonObject) + { + return new TextSetting() { Value = jsonObject["value"]?.GetValue() ?? string.Empty }; + } + + public override void Update(JsonObject payload) + { + Value = payload[Key]?.GetValue() ?? string.Empty; + } +} diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/ToggleSetting.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/ToggleSetting.cs new file mode 100644 index 0000000000..0203671227 --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/ToggleSetting.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.CmdPal.Extensions.Helpers; + +public sealed class ToggleSetting : Setting +{ + private ToggleSetting() + : base() + { + } + + public ToggleSetting(string key, bool defaultValue) + : base(key, defaultValue) + { + } + + public ToggleSetting(string key, string label, string description, bool defaultValue) + : base(key, label, description, defaultValue) + { + } + + public override Dictionary ToDictionary() + { + return new Dictionary + { + { "type", "Input.Toggle" }, + { "title", Label }, + { "id", Key }, + { "label", Description }, + { "value", JsonSerializer.Serialize(Value) }, + { "isRequired", IsRequired }, + { "errorMessage", ErrorMessage }, + }; + } + + public static ToggleSetting LoadFromJson(JsonObject jsonObject) + { + return new ToggleSetting() { Value = jsonObject["value"]?.GetValue() ?? false }; + } + + public override void Update(JsonObject payload) + { + // Adaptive cards returns boolean values as a string "true"/"false", cause of course. + var strFromJson = payload[Key]?.GetValue() ?? string.Empty; + var val = strFromJson switch { "true" => true, "false" => false, _ => false }; + Value = val; + } +} diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions/Microsoft.CmdPal.Extensions.idl b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions/Microsoft.CmdPal.Extensions.idl index 6a272afecc..b2ca0314fb 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions/Microsoft.CmdPal.Extensions.idl +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions/Microsoft.CmdPal.Extensions.idl @@ -246,13 +246,18 @@ namespace Microsoft.CmdPal.Extensions IForm[] Forms(); } + [contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)] + interface ICommandSettings { + IFormPage SettingsPage { get; }; + }; + [contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)] interface ICommandProvider requires Windows.Foundation.IClosable { String DisplayName { get; }; IconDataType Icon { get; }; + ICommandSettings Settings { get; }; // TODO! Boolean CanBeCached { get; }; - // TODO! IFormPage SettingsPage { get; }; IListItem[] TopLevelCommands();