mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
Add helper classes for very simple settings (#141)
This updates the spec to enable CmdPal to host settings for extensions. Extensions can provide us essentially, a FormPage, and we'll give that special treatment as _the settings_ for the extension. We're gonna use this in #100 in the future. For now, I also added a helper set of classes for quickly constructing a `Settings` object, which is basically just a dictionary. Notably though, it lets us define simple control types for each of these settings, which we can then turn into the `IFormPage` on the developer's behalf. See the [`SampleSettingsPage.cs`](https://github.com/zadjii-msft/PowerToys/compare/main...dev/migrie/s/settings-for-extensions#diff-ac06e39258579222e94539315ad59e0bf04f3b0f3e83a2f8a11aa5a42d569ebe) for an example. Closes #123
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<bool>("onOff");
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = $"SampleSettingsPage: Changed the value of onOff to {onOff}" });
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"SamplePagesExtension (Package)": {
|
||||
"commandName": "MsixPackage"
|
||||
"commandName": "MsixPackage",
|
||||
"doNotLaunchApp": true
|
||||
},
|
||||
"SamplePagesExtension (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<bool>("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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string, object> ToDictionary();
|
||||
|
||||
public string ToDataIdentifier();
|
||||
}
|
||||
@@ -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<T> : 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<string, object> 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);
|
||||
}
|
||||
@@ -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<string, object> _settings = new();
|
||||
private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true };
|
||||
|
||||
public event TypedEventHandler<object, Settings>? SettingsChanged;
|
||||
|
||||
public void Add<T>(Setting<T> s)
|
||||
{
|
||||
_settings.Add(s.Key, s);
|
||||
}
|
||||
|
||||
public T? GetSetting<T>(string key)
|
||||
{
|
||||
return _settings[key] is Setting<T> s ? s.Value : default;
|
||||
}
|
||||
|
||||
public bool TryGetSetting<T>(string key, out T? val)
|
||||
{
|
||||
object? o;
|
||||
if (_settings.TryGetValue(key, out o))
|
||||
{
|
||||
if (o is Setting<T> 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<string>
|
||||
{
|
||||
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<string, object> ToDictionary()
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "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>() ?? string.Empty };
|
||||
}
|
||||
|
||||
public override void Update(JsonObject payload)
|
||||
{
|
||||
Value = payload[Key]?.GetValue<string>() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -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<bool>
|
||||
{
|
||||
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<string, object> ToDictionary()
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "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<bool>() ?? 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>() ?? string.Empty;
|
||||
var val = strFromJson switch { "true" => true, "false" => false, _ => false };
|
||||
Value = val;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user