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:
Mike Griese
2024-11-12 05:37:51 -08:00
committed by GitHub
parent 4a23ba25ac
commit b96ab5e871
24 changed files with 564 additions and 40 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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}" });
}
}

View File

@@ -1,7 +1,8 @@
{
"profiles": {
"SamplePagesExtension (Package)": {
"commandName": "MsixPackage"
"commandName": "MsixPackage",
"doNotLaunchApp": true
},
"SamplePagesExtension (Unpackaged)": {
"commandName": "Project"

View File

@@ -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;
}

View File

@@ -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",
}
];

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 = """

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();