mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 04:00:02 +01:00
BREAKING: Add ContentPages and Invoke(sender) (#395)
## BREAKING API CHANGES APPROACHING Closes #307 Closes #238 Removes `Command` from `ITag` Adds `IDetailsCommand` (to achieve the same goal as the above originally had) Adds `ITreeContent`, based on a hunch Adds `ShowToast` and `Confirm` to `CommandResult` too, but without UX yet Extensions from before this change will need to be updated. * The `InvokableCommand` change should be trivial - the helpers should abstract that delta away for you. * The `ContentPage` change should be pretty easy to make. * Both `MarkdownPage` and `FormPage` are now just `ContentPage` * `FormPage.Forms()` -> `ContentPage.GetContent()` * `MarkdownPage.Bodies()` -> `ContentPage.GetContent()` * `IForm`s become `IFormContent`. Methods become properties (not that bad) * I'm only deprecating the old Markdown and Form pages - I'll fully remove them before we OSS, but I'll give everyone a couple weeks to port them. * No one was using `Tag.Command` and it always seemed iffy at best - better not.
This commit is contained in:
@@ -3,14 +3,13 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
namespace PokedexExtension;
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "This is sample code")]
|
||||
public class Pokemon
|
||||
{
|
||||
public int Number { get; set; }
|
||||
@@ -21,6 +20,8 @@ public class Pokemon
|
||||
|
||||
public string IconUrl => $"https://serebii.net/pokedex-sv/icon/new/{Number:D3}.png";
|
||||
|
||||
public string SerebiiUrl => $"https://serebii.net/pokedex-sv/{Number:D3}.shtml";
|
||||
|
||||
public Pokemon(int number, string name, List<string> types)
|
||||
{
|
||||
Number = number;
|
||||
@@ -29,17 +30,42 @@ public class Pokemon
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
|
||||
internal sealed partial class PokemonPage : NoOpCommand
|
||||
internal sealed partial class OpenPokemonCommand : InvokableCommand
|
||||
{
|
||||
public PokemonPage(Pokemon pokemon)
|
||||
public OpenPokemonCommand()
|
||||
{
|
||||
Name = pokemon.Name;
|
||||
Icon = new(pokemon.IconUrl);
|
||||
Name = "Open";
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke(object sender)
|
||||
{
|
||||
if (sender is PokemonListItem item)
|
||||
{
|
||||
var pokemon = item.Pokemon;
|
||||
Process.Start(new ProcessStartInfo(pokemon.SerebiiUrl) { UseShellExecute = true });
|
||||
}
|
||||
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class PokemonListItem : ListItem
|
||||
{
|
||||
private static readonly OpenPokemonCommand _command = new();
|
||||
|
||||
public Pokemon Pokemon { get; private set; }
|
||||
|
||||
public PokemonListItem(Pokemon p)
|
||||
: base(_command)
|
||||
{
|
||||
Pokemon = p;
|
||||
Title = Pokemon.Name;
|
||||
Icon = new(Pokemon.IconUrl);
|
||||
Subtitle = $"#{Pokemon.Number}";
|
||||
Tags = Pokemon.Types.Select(t => new Tag() { Text = t, Background = PokedexExtensionPage.GetColorForType(t) }).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
|
||||
internal sealed partial class PokedexExtensionPage : ListPage
|
||||
{
|
||||
private readonly List<Pokemon> _kanto =
|
||||
@@ -448,14 +474,7 @@ internal sealed partial class PokedexExtensionPage : ListPage
|
||||
|
||||
public override IListItem[] GetItems() => _kanto.AsEnumerable().Concat(_johto.AsEnumerable()).Concat(_hoenn.AsEnumerable()).Select(GetPokemonListItem).ToArray();
|
||||
|
||||
private static ListItem GetPokemonListItem(Pokemon pokemon)
|
||||
{
|
||||
return new ListItem(new PokemonPage(pokemon))
|
||||
{
|
||||
Subtitle = $"#{pokemon.Number}",
|
||||
Tags = pokemon.Types.Select(t => new Tag() { Text = t, Background = GetColorForType(t) }).ToArray(),
|
||||
};
|
||||
}
|
||||
private static ListItem GetPokemonListItem(Pokemon pokemon) => new PokemonListItem(pokemon);
|
||||
|
||||
// Dictionary mapping Pokémon types to their corresponding colors
|
||||
private static readonly Dictionary<string, OptionalColor> TypeColors = new()
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
internal sealed partial class SampleContentPage : ContentPage
|
||||
{
|
||||
private readonly SampleContentForm sampleForm = new();
|
||||
private readonly MarkdownContent sampleMarkdown = new() { Body = "# Sample page with mixed content \n This page has both markdown, and form content" };
|
||||
|
||||
public override IContent[] GetContent() => [sampleMarkdown, sampleForm];
|
||||
|
||||
public SampleContentPage()
|
||||
{
|
||||
Name = "Sample Content";
|
||||
Icon = new(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
|
||||
internal sealed partial class SampleContentForm : FormContent
|
||||
{
|
||||
public SampleContentForm()
|
||||
{
|
||||
TemplateJson = $$"""
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "medium",
|
||||
"weight": "bolder",
|
||||
"text": " ${ParticipantInfoForm.title}",
|
||||
"horizontalAlignment": "center",
|
||||
"wrap": true,
|
||||
"style": "heading"
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"label": "Name",
|
||||
"style": "text",
|
||||
"id": "SimpleVal",
|
||||
"isRequired": true,
|
||||
"errorMessage": "Name is required",
|
||||
"placeholder": "Enter your name"
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"label": "Homepage",
|
||||
"style": "url",
|
||||
"id": "UrlVal",
|
||||
"placeholder": "Enter your homepage url"
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"label": "Email",
|
||||
"style": "email",
|
||||
"id": "EmailVal",
|
||||
"placeholder": "Enter your email"
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"label": "Phone",
|
||||
"style": "tel",
|
||||
"id": "TelVal",
|
||||
"placeholder": "Enter your phone number"
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"label": "Comments",
|
||||
"style": "text",
|
||||
"isMultiline": true,
|
||||
"id": "MultiLineVal",
|
||||
"placeholder": "Enter any comments"
|
||||
},
|
||||
{
|
||||
"type": "Input.Number",
|
||||
"label": "Quantity (Minimum -5, Maximum 5)",
|
||||
"min": -5,
|
||||
"max": 5,
|
||||
"value": 1,
|
||||
"id": "NumVal",
|
||||
"errorMessage": "The quantity must be between -5 and 5"
|
||||
},
|
||||
{
|
||||
"type": "Input.Date",
|
||||
"label": "Due Date",
|
||||
"id": "DateVal",
|
||||
"value": "2017-09-20"
|
||||
},
|
||||
{
|
||||
"type": "Input.Time",
|
||||
"label": "Start time",
|
||||
"id": "TimeVal",
|
||||
"value": "16:59"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "medium",
|
||||
"weight": "bolder",
|
||||
"text": "${Survey.title} ",
|
||||
"horizontalAlignment": "center",
|
||||
"wrap": true,
|
||||
"style": "heading"
|
||||
},
|
||||
{
|
||||
"type": "Input.ChoiceSet",
|
||||
"id": "CompactSelectVal",
|
||||
"label": "${Survey.questions[0].question}",
|
||||
"style": "compact",
|
||||
"value": "1",
|
||||
"choices": [
|
||||
{
|
||||
"$data": "${Survey.questions[0].items}",
|
||||
"title": "${choice}",
|
||||
"value": "${value}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Input.ChoiceSet",
|
||||
"id": "SingleSelectVal",
|
||||
"label": "${Survey.questions[1].question}",
|
||||
"style": "expanded",
|
||||
"value": "1",
|
||||
"choices": [
|
||||
{
|
||||
"$data": "${Survey.questions[1].items}",
|
||||
"title": "${choice}",
|
||||
"value": "${value}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Input.ChoiceSet",
|
||||
"id": "MultiSelectVal",
|
||||
"label": "${Survey.questions[2].question}",
|
||||
"isMultiSelect": true,
|
||||
"value": "1,3",
|
||||
"choices": [
|
||||
{
|
||||
"$data": "${Survey.questions[2].items}",
|
||||
"title": "${choice}",
|
||||
"value": "${value}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "medium",
|
||||
"weight": "bolder",
|
||||
"text": "Input.Toggle",
|
||||
"horizontalAlignment": "center",
|
||||
"wrap": true,
|
||||
"style": "heading"
|
||||
},
|
||||
{
|
||||
"type": "Input.Toggle",
|
||||
"label": "Please accept the terms and conditions:",
|
||||
"title": "${Survey.questions[3].question}",
|
||||
"valueOn": "true",
|
||||
"valueOff": "false",
|
||||
"id": "AcceptsTerms",
|
||||
"isRequired": true,
|
||||
"errorMessage": "Accepting the terms and conditions is required"
|
||||
},
|
||||
{
|
||||
"type": "Input.Toggle",
|
||||
"label": "How do you feel about red cars?",
|
||||
"title": "${Survey.questions[4].question}",
|
||||
"valueOn": "RedCars",
|
||||
"valueOff": "NotRedCars",
|
||||
"id": "ColorPreference"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Submit",
|
||||
"data": {
|
||||
"id": "1234567890"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Action.ShowCard",
|
||||
"title": "Show Card",
|
||||
"card": {
|
||||
"type": "AdaptiveCard",
|
||||
"body": [
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"label": "Enter comment",
|
||||
"style": "text",
|
||||
"id": "CommentVal"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "OK"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
DataJson = $$"""
|
||||
{
|
||||
"ParticipantInfoForm": {
|
||||
"title": "Input.Text elements"
|
||||
},
|
||||
"Survey": {
|
||||
"title": "Input ChoiceSet",
|
||||
"questions": [
|
||||
{
|
||||
"question": "What color do you want? (compact)",
|
||||
"items": [
|
||||
{
|
||||
"choice": "Red",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"choice": "Green",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"choice": "Blue",
|
||||
"value": "3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "What color do you want? (expanded)",
|
||||
"items": [
|
||||
{
|
||||
"choice": "Red",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"choice": "Green",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"choice": "Blue",
|
||||
"value": "3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "What color do you want? (multiselect)",
|
||||
"items": [
|
||||
{
|
||||
"choice": "Red",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"choice": "Green",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"choice": "Blue",
|
||||
"value": "3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "I accept the terms and conditions (True/False)"
|
||||
},
|
||||
{
|
||||
"question": "Red cars are better than other cars"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var formInput = JsonNode.Parse(payload)?.AsObject();
|
||||
if (formInput == null)
|
||||
{
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
|
||||
// Application.Current.GetService<ILocalSettingsService>().SaveSettingAsync("GlobalHotkey", formInput["hotkey"]?.ToString() ?? string.Empty);
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
|
||||
internal sealed partial class SampleTreeContentPage : ContentPage
|
||||
{
|
||||
private readonly TreeContent myContentTree;
|
||||
|
||||
public override IContent[] GetContent() => [myContentTree];
|
||||
|
||||
public SampleTreeContentPage()
|
||||
{
|
||||
Name = Title = "Sample Content";
|
||||
Icon = new("\uE81E");
|
||||
|
||||
myContentTree = new()
|
||||
{
|
||||
RootContent = new MarkdownContent() { Body = "# This page has nested content" },
|
||||
Children = [
|
||||
new TreeContent()
|
||||
{
|
||||
RootContent = new MarkdownContent() { Body = "Yo dog" },
|
||||
Children = [
|
||||
new TreeContent()
|
||||
{
|
||||
RootContent = new MarkdownContent() { Body = "I heard you like content" },
|
||||
Children = [
|
||||
new MarkdownContent() { Body = "So we put content in your content" },
|
||||
new FormContent() { TemplateJson = "{\"$schema\":\"http://adaptivecards.io/schemas/adaptive-card.json\",\"type\":\"AdaptiveCard\",\"version\":\"1.6\",\"body\":[{\"type\":\"TextBlock\",\"size\":\"medium\",\"weight\":\"bolder\",\"text\":\"Mix and match why don't you\",\"horizontalAlignment\":\"center\",\"wrap\":true,\"style\":\"heading\"},{\"type\":\"TextBlock\",\"text\":\"You can have forms here too\",\"horizontalAlignment\":\"Right\",\"wrap\":true}],\"actions\":[{\"type\":\"Action.Submit\",\"title\":\"It's a form, you get it\",\"data\":{\"id\":\"LoginVal\"}}]}" },
|
||||
new MarkdownContent() { Body = "Another markdown down here" },
|
||||
],
|
||||
},
|
||||
new MarkdownContent() { Body = "**slaps roof**" },
|
||||
new MarkdownContent() { Body = "This baby can fit so much content" },
|
||||
|
||||
],
|
||||
}
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ internal sealed partial class SampleListPage : ListPage
|
||||
{
|
||||
public SampleListPage()
|
||||
{
|
||||
Icon = new(string.Empty);
|
||||
Icon = new("\uEA37");
|
||||
Name = "Sample List Page";
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ internal sealed partial class SampleListPageWithDetails : ListPage
|
||||
{
|
||||
public SampleListPageWithDetails()
|
||||
{
|
||||
Icon = new(string.Empty);
|
||||
Name = "Sample List Page with Details";
|
||||
Icon = new("\uE8A0");
|
||||
Name = Title = "Sample List Page with Details";
|
||||
this.ShowDetails = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,59 +10,85 @@ namespace SamplePagesExtension;
|
||||
public partial class SamplesListPage : ListPage
|
||||
{
|
||||
private readonly IListItem[] _commands = [
|
||||
new ListItem(new SampleListPage())
|
||||
{
|
||||
Title = "List Page Sample Command",
|
||||
Subtitle = "Display a list of items",
|
||||
},
|
||||
new ListItem(new SampleListPageWithDetails())
|
||||
{
|
||||
Title = "List Page With Details",
|
||||
Subtitle = "A list of items, each with additional details to display",
|
||||
},
|
||||
new ListItem(new SampleUpdatingItemsPage())
|
||||
{
|
||||
Title = "List page with items that change",
|
||||
Subtitle = "The items on the list update themselves in real time",
|
||||
},
|
||||
new ListItem(new SampleDynamicListPage())
|
||||
{
|
||||
Title = "Dynamic List Page Command",
|
||||
Subtitle = "Changes the list of items in response to the typed query",
|
||||
},
|
||||
|
||||
new ListItem(new SampleMarkdownPage())
|
||||
{
|
||||
Title = "Markdown Page Sample Command",
|
||||
Subtitle = "Display a page of rendered markdown",
|
||||
},
|
||||
new ListItem(new SampleMarkdownManyBodies())
|
||||
{
|
||||
Title = "Markdown with multiple blocks",
|
||||
Subtitle = "A page with multiple blocks of rendered markdown",
|
||||
},
|
||||
new ListItem(new SampleMarkdownDetails())
|
||||
{
|
||||
Title = "Markdown with details",
|
||||
Subtitle = "A page with markdown and details",
|
||||
},
|
||||
// List pages
|
||||
new ListItem(new SampleListPage())
|
||||
{
|
||||
Title = "List Page Sample Command",
|
||||
Subtitle = "Display a list of items",
|
||||
},
|
||||
new ListItem(new SampleListPageWithDetails())
|
||||
{
|
||||
Title = "List Page With Details",
|
||||
Subtitle = "A list of items, each with additional details to display",
|
||||
},
|
||||
new ListItem(new SampleUpdatingItemsPage())
|
||||
{
|
||||
Title = "List page with items that change",
|
||||
Subtitle = "The items on the list update themselves in real time",
|
||||
},
|
||||
new ListItem(new SampleDynamicListPage())
|
||||
{
|
||||
Title = "Dynamic List Page Command",
|
||||
Subtitle = "Changes the list of items in response to the typed query",
|
||||
},
|
||||
|
||||
new ListItem(new SampleFormPage())
|
||||
{
|
||||
Title = "Form Page Sample Command",
|
||||
Subtitle = "Define inputs to retrieve input from the user",
|
||||
},
|
||||
new ListItem(new SampleSettingsPage())
|
||||
{
|
||||
Title = "Sample settings page",
|
||||
Subtitle = "A demo of the settings helpers",
|
||||
},
|
||||
// Content pages
|
||||
new ListItem(new SampleContentPage())
|
||||
{
|
||||
Title = "Sample content page",
|
||||
Subtitle = "Display mixed forms, markdown, and other types of content",
|
||||
},
|
||||
new ListItem(new SampleTreeContentPage())
|
||||
{
|
||||
Title = "Sample nested content",
|
||||
Subtitle = "Example of nesting a tree of content",
|
||||
},
|
||||
new ListItem(new SampleCommentsPage())
|
||||
{
|
||||
Title = "Sample of nested comments",
|
||||
Subtitle = "Demo of using nested trees of content to create a comment thread-like experience",
|
||||
Icon = new("\uE90A"), // Comment
|
||||
},
|
||||
|
||||
new ListItem(new EvilSamplesPage())
|
||||
{
|
||||
Title = "Evil samples",
|
||||
Subtitle = "Samples designed to break the palette in many different evil ways",
|
||||
}
|
||||
// DEPRECATED: Markdown pages
|
||||
new ListItem(new SampleMarkdownPage())
|
||||
{
|
||||
Title = "Markdown Page Sample Command",
|
||||
Subtitle = "Display a page of rendered markdown",
|
||||
},
|
||||
new ListItem(new SampleMarkdownManyBodies())
|
||||
{
|
||||
Title = "Markdown with multiple blocks",
|
||||
Subtitle = "A page with multiple blocks of rendered markdown",
|
||||
},
|
||||
new ListItem(new SampleMarkdownDetails())
|
||||
{
|
||||
Title = "Markdown with details",
|
||||
Subtitle = "A page with markdown and details",
|
||||
},
|
||||
|
||||
// DEPRECATED: Form pages
|
||||
new ListItem(new SampleFormPage())
|
||||
{
|
||||
Title = "Form Page Sample Command",
|
||||
Subtitle = "Define inputs to retrieve input from the user",
|
||||
},
|
||||
|
||||
// Settings helpers
|
||||
new ListItem(new SampleSettingsPage())
|
||||
{
|
||||
Title = "Sample settings page",
|
||||
Subtitle = "A demo of the settings helpers",
|
||||
},
|
||||
|
||||
// Evil edge cases
|
||||
// Anything weird that might break the palette - put that in here.
|
||||
new ListItem(new EvilSamplesPage())
|
||||
{
|
||||
Title = "Evil samples",
|
||||
Subtitle = "Samples designed to break the palette in many different evil ways",
|
||||
}
|
||||
];
|
||||
|
||||
public SamplesListPage()
|
||||
|
||||
@@ -13,6 +13,6 @@ public partial class SelfImmolateCommand : InvokableCommand
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
Process.GetCurrentProcess().Kill();
|
||||
return base.Invoke();
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,5 +78,6 @@ public partial class ActionBarViewModel : ObservableObject,
|
||||
|
||||
// InvokeItemCommand is what this will be in Xaml due to source generator
|
||||
[RelayCommand]
|
||||
private void InvokeItem(CommandContextItemViewModel item) => WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command));
|
||||
private void InvokeItem(CommandContextItemViewModel item) =>
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command, item.Model));
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, IPageContext context) : CommandItemViewModel(new(contextItem), context)
|
||||
{
|
||||
private readonly ExtensionObject<ICommandContextItem> _contextItemModel = new(contextItem);
|
||||
public ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
|
||||
|
||||
public bool IsCritical { get; private set; }
|
||||
|
||||
@@ -19,7 +19,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
|
||||
{
|
||||
base.InitializeProperties();
|
||||
|
||||
var contextItem = _contextItemModel.Unsafe;
|
||||
var contextItem = Model.Unsafe;
|
||||
if (contextItem == null)
|
||||
{
|
||||
return; // throw?
|
||||
|
||||
@@ -179,12 +179,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel
|
||||
break;
|
||||
case nameof(Icon):
|
||||
var listIcon = model.Icon;
|
||||
var iconInfo = listIcon != null ? listIcon : Command.Unsafe!.Icon;
|
||||
var iconInfo = listIcon ?? Command.Unsafe!.Icon;
|
||||
Icon = new(iconInfo);
|
||||
Icon.InitializeProperties();
|
||||
break;
|
||||
|
||||
// TODO! MoreCommands array, which needs to also raise HasMoreCommands
|
||||
// TODO GH #360 - make MoreCommands observable
|
||||
// which needs to also raise HasMoreCommands
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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 AdaptiveCards.ObjectModel.WinUI3;
|
||||
using AdaptiveCards.Templating;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Windows.Data.Json;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ContentFormViewModel(IFormContent _form, IPageContext context) :
|
||||
ContentViewModel(context)
|
||||
{
|
||||
private readonly ExtensionObject<IFormContent> _formModel = new(_form);
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
public string TemplateJson { get; protected set; } = "{}";
|
||||
|
||||
public string StateJson { get; protected set; } = "{}";
|
||||
|
||||
public string DataJson { get; protected set; } = "{}";
|
||||
|
||||
public AdaptiveCardParseResult? Card { get; private set; }
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = _formModel.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TemplateJson = model.TemplateJson;
|
||||
StateJson = model.StateJson;
|
||||
DataJson = model.DataJson;
|
||||
|
||||
AdaptiveCardTemplate template = new(TemplateJson);
|
||||
var cardJson = template.Expand(DataJson);
|
||||
Card = AdaptiveCard.FromJsonString(cardJson);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// If we fail to parse the card JSON, then display _our own card_
|
||||
// with the exception
|
||||
AdaptiveCardTemplate template = new(ErrorCardJson);
|
||||
|
||||
// todo: we could probably stick Card.Errors in there too
|
||||
var dataJson = $$"""
|
||||
{
|
||||
"error_message": {{JsonSerializer.Serialize(e.Message)}},
|
||||
"error_stack": {{JsonSerializer.Serialize(e.StackTrace)}},
|
||||
"inner_exception": {{JsonSerializer.Serialize(e.InnerException?.Message)}},
|
||||
"template_json": {{JsonSerializer.Serialize(TemplateJson)}},
|
||||
"data_json": {{JsonSerializer.Serialize(DataJson)}}
|
||||
}
|
||||
""";
|
||||
var cardJson = template.Expand(dataJson);
|
||||
Card = AdaptiveCard.FromJsonString(cardJson);
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
}
|
||||
|
||||
public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs)
|
||||
{
|
||||
if (action is AdaptiveOpenUrlAction openUrlAction)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
|
||||
return;
|
||||
}
|
||||
|
||||
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
|
||||
{
|
||||
// Get the data and inputs
|
||||
// var data = submitAction.DataJson.Stringify();
|
||||
var inputString = inputs.Stringify();
|
||||
|
||||
// _ = data;
|
||||
_ = inputString;
|
||||
|
||||
try
|
||||
{
|
||||
var model = _formModel.Unsafe!;
|
||||
if (model != null)
|
||||
{
|
||||
var result = model.SubmitForm(inputString);
|
||||
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PageContext.ShowException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string ErrorCardJson = """
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.5",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Error parsing form from extension",
|
||||
"wrap": true,
|
||||
"style": "heading",
|
||||
"size": "ExtraLarge",
|
||||
"weight": "Bolder",
|
||||
"color": "Attention"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"wrap": true,
|
||||
"text": "${error_message}",
|
||||
"color": "Attention"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${error_stack}",
|
||||
"fontType": "Monospace"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"wrap": true,
|
||||
"text": "Inner exception:"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"wrap": true,
|
||||
"text": "${inner_exception}",
|
||||
"color": "Attention"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using AdaptiveCards.ObjectModel.WinUI3;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, IPageContext context) :
|
||||
ContentViewModel(context)
|
||||
{
|
||||
public ExtensionObject<IMarkdownContent> Model { get; } = new(_markdown);
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
public string Body { get; protected set; } = string.Empty;
|
||||
|
||||
public AdaptiveCardParseResult? Card { get; private set; }
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Body = model.Body;
|
||||
UpdateProperty(nameof(Body));
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
private void Model_PropChanged(object sender, PropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var propName = args.PropertyName;
|
||||
FetchProperty(propName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PageContext.ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Body):
|
||||
Body = model.Body;
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ContentPageViewModel : PageViewModel
|
||||
{
|
||||
private readonly ExtensionObject<IContentPage> _model;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ContentViewModel> Content { get; set; } = [];
|
||||
|
||||
public List<CommandContextItemViewModel> Commands { get; private set; } = [];
|
||||
|
||||
public bool HasCommands => Commands.Count > 0;
|
||||
|
||||
public DetailsViewModel? Details { get; private set; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Details))]
|
||||
public bool HasDetails => Details != null;
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, CommandPaletteHost host)
|
||||
: base(model, scheduler, host)
|
||||
{
|
||||
_model = new(model);
|
||||
}
|
||||
|
||||
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
|
||||
private void Model_ItemsChanged(object sender, ItemsChangedEventArgs args) => FetchContent();
|
||||
|
||||
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
|
||||
private void FetchContent()
|
||||
{
|
||||
List<ContentViewModel> newContent = [];
|
||||
try
|
||||
{
|
||||
var newItems = _model.Unsafe!.GetContent();
|
||||
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
var viewModel = ViewModelFromContent(item, PageContext);
|
||||
if (viewModel != null)
|
||||
{
|
||||
viewModel.InitializeProperties();
|
||||
newContent.Add(viewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex, _model?.Unsafe?.Name);
|
||||
throw;
|
||||
}
|
||||
|
||||
// Now, back to a UI thread to update the observable collection
|
||||
Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(Content, newContent);
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
PageContext.Scheduler);
|
||||
}
|
||||
|
||||
public static ContentViewModel? ViewModelFromContent(IContent content, IPageContext context)
|
||||
{
|
||||
ContentViewModel? viewModel = content switch
|
||||
{
|
||||
IFormContent form => new ContentFormViewModel(form, context),
|
||||
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
|
||||
ITreeContent tree => new ContentTreeViewModel(tree, context),
|
||||
_ => null,
|
||||
};
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
base.InitializeProperties();
|
||||
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
Commands = model.Commands
|
||||
.Where(contextItem => contextItem is ICommandContextItem)
|
||||
.Select(contextItem => (contextItem as ICommandContextItem)!)
|
||||
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
|
||||
.ToList();
|
||||
|
||||
var extensionDetails = model.Details;
|
||||
if (extensionDetails != null)
|
||||
{
|
||||
Details = new(extensionDetails, PageContext);
|
||||
Details.InitializeProperties();
|
||||
}
|
||||
|
||||
UpdateDetails();
|
||||
|
||||
FetchContent();
|
||||
model.ItemsChanged += Model_ItemsChanged;
|
||||
}
|
||||
|
||||
protected override void FetchProperty(string propertyName)
|
||||
{
|
||||
base.FetchProperty(propertyName);
|
||||
|
||||
var model = this._model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
// case nameof(Commands):
|
||||
// TODO GH #360 - make MoreCommands observable
|
||||
// this.ShowDetails = model.ShowDetails;
|
||||
// break;
|
||||
case nameof(Details):
|
||||
var extensionDetails = model.Details;
|
||||
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
|
||||
UpdateDetails();
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
}
|
||||
|
||||
private void UpdateDetails()
|
||||
{
|
||||
UpdateProperty(nameof(Details));
|
||||
UpdateProperty(nameof(HasDetails));
|
||||
|
||||
Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
if (HasDetails)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(Details));
|
||||
}
|
||||
else
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
}
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
PageContext.Scheduler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ContentTreeViewModel(ITreeContent _tree, IPageContext context) :
|
||||
ContentViewModel(context)
|
||||
{
|
||||
public ExtensionObject<ITreeContent> Model { get; } = new(_tree);
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
public ContentViewModel? RootContent { get; protected set; }
|
||||
|
||||
public ObservableCollection<ContentViewModel> Children { get; } = [];
|
||||
|
||||
public bool HasChildren => Children.Count > 0;
|
||||
|
||||
public ObservableCollection<ContentViewModel> StupidGames => [RootContent];
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var root = model.RootContent;
|
||||
if (root != null)
|
||||
{
|
||||
RootContent = ContentPageViewModel.ViewModelFromContent(root, PageContext);
|
||||
RootContent?.InitializeProperties();
|
||||
UpdateProperty(nameof(RootContent));
|
||||
UpdateProperty(nameof(StupidGames));
|
||||
}
|
||||
|
||||
FetchContent();
|
||||
model.PropChanged += Model_PropChanged;
|
||||
model.ItemsChanged += Model_ItemsChanged;
|
||||
}
|
||||
|
||||
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
|
||||
private void Model_ItemsChanged(object sender, ItemsChangedEventArgs args) => FetchContent();
|
||||
|
||||
private void Model_PropChanged(object sender, PropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var propName = args.PropertyName;
|
||||
FetchProperty(propName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PageContext.ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(RootContent):
|
||||
var root = model.RootContent;
|
||||
if (root != null)
|
||||
{
|
||||
RootContent = ContentPageViewModel.ViewModelFromContent(root, PageContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
root = null;
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(StupidGames));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
}
|
||||
|
||||
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
|
||||
private void FetchContent()
|
||||
{
|
||||
List<ContentViewModel> newContent = [];
|
||||
try
|
||||
{
|
||||
var newItems = Model.Unsafe!.GetChildren();
|
||||
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
var viewModel = ContentPageViewModel.ViewModelFromContent(item, PageContext);
|
||||
if (viewModel != null)
|
||||
{
|
||||
viewModel.InitializeProperties();
|
||||
newContent.Add(viewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PageContext.ShowException(ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
// Now, back to a UI thread to update the observable collection
|
||||
Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(Children, newContent);
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
PageContext.Scheduler);
|
||||
|
||||
UpdateProperty(nameof(HasChildren));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public abstract partial class ContentViewModel(IPageContext context) :
|
||||
ExtensionObjectViewModel(context)
|
||||
{
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class ListItemViewModel(IListItem model, IPageContext context)
|
||||
: CommandItemViewModel(new(model), context)
|
||||
{
|
||||
private readonly ExtensionObject<IListItem> _listItemModel = new(model);
|
||||
public ExtensionObject<IListItem> Model { get; } = new(model);
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
@@ -33,7 +33,7 @@ public partial class ListItemViewModel(IListItem model, IPageContext context)
|
||||
{
|
||||
base.InitializeProperties();
|
||||
|
||||
var li = _listItemModel.Unsafe;
|
||||
var li = Model.Unsafe;
|
||||
if (li == null)
|
||||
{
|
||||
return; // throw?
|
||||
@@ -67,7 +67,7 @@ public partial class ListItemViewModel(IListItem model, IPageContext context)
|
||||
{
|
||||
base.FetchProperty(propertyName);
|
||||
|
||||
var model = this._listItemModel.Unsafe;
|
||||
var model = this.Model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
@@ -108,7 +108,7 @@ public partial class ListItemViewModel(IListItem model, IPageContext context)
|
||||
|
||||
public override string ToString() => $"{Name} ListItemViewModel";
|
||||
|
||||
public override bool Equals(object? obj) => obj is ListItemViewModel vm && vm._listItemModel.Equals(this._listItemModel);
|
||||
public override bool Equals(object? obj) => obj is ListItemViewModel vm && vm.Model.Equals(this.Model);
|
||||
|
||||
public override int GetHashCode() => _listItemModel.GetHashCode();
|
||||
public override int GetHashCode() => Model.GetHashCode();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public partial class ListViewModel : PageViewModel
|
||||
|
||||
public string ModelPlaceholderText { get => string.IsNullOrEmpty(field) ? "Type here to search..." : field; private set; } = string.Empty;
|
||||
|
||||
public override string PlaceholderText { get => ModelPlaceholderText; }
|
||||
public override string PlaceholderText => ModelPlaceholderText;
|
||||
|
||||
public string SearchText { get; private set; } = string.Empty;
|
||||
|
||||
@@ -49,6 +49,7 @@ public partial class ListViewModel : PageViewModel
|
||||
_model = new(model);
|
||||
}
|
||||
|
||||
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
|
||||
private void Model_ItemsChanged(object sender, ItemsChangedEventArgs args) => FetchItems();
|
||||
|
||||
protected override void OnFilterUpdated(string filter)
|
||||
@@ -194,14 +195,14 @@ public partial class ListViewModel : PageViewModel
|
||||
// InvokeItemCommand is what this will be in Xaml due to source generator
|
||||
[RelayCommand]
|
||||
private void InvokeItem(ListItemViewModel item) =>
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command));
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command, item.Model));
|
||||
|
||||
[RelayCommand]
|
||||
private void InvokeSecondaryCommand(ListItemViewModel item)
|
||||
{
|
||||
if (item.SecondaryCommand != null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.SecondaryCommand.Command));
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.SecondaryCommand.Command, item.Model));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
@@ -38,7 +39,7 @@ public partial class MarkdownPageViewModel : PageViewModel
|
||||
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
|
||||
private void FetchContent()
|
||||
{
|
||||
List<string> newBodies = new();
|
||||
List<string> newBodies = [];
|
||||
try
|
||||
{
|
||||
var newItems = _model.Unsafe!.Bodies();
|
||||
|
||||
@@ -10,6 +10,27 @@ namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
/// <summary>
|
||||
/// Used to do a command - navigate to a page or invoke it
|
||||
/// </summary>
|
||||
public record PerformCommandMessage(ExtensionObject<ICommand> Command)
|
||||
public record PerformCommandMessage
|
||||
{
|
||||
public ExtensionObject<ICommand> Command { get; }
|
||||
|
||||
public object? Context { get; }
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command)
|
||||
{
|
||||
Command = command;
|
||||
Context = null;
|
||||
}
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<IListItem> context)
|
||||
{
|
||||
Command = command;
|
||||
Context = context.Unsafe;
|
||||
}
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<ICommandContextItem> context)
|
||||
{
|
||||
Command = command;
|
||||
Context = context.Unsafe;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ public partial class TagViewModel(ITag _tag, IPageContext context) : ExtensionOb
|
||||
return;
|
||||
}
|
||||
|
||||
Command = new(model.Command);
|
||||
Text = model.Text;
|
||||
Foreground = model.Foreground;
|
||||
Background = model.Background;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
public partial class ContentTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
// Define the (currently empty) data templates to return
|
||||
// These will be "filled-in" in the XAML code.
|
||||
public DataTemplate? FormTemplate { get; set; }
|
||||
|
||||
public DataTemplate? MarkdownTemplate { get; set; }
|
||||
|
||||
public DataTemplate? TreeTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item)
|
||||
{
|
||||
if (item is ContentViewModel element)
|
||||
{
|
||||
var data = element;
|
||||
return data switch
|
||||
{
|
||||
ContentFormViewModel => FormTemplate,
|
||||
ContentMarkdownViewModel => MarkdownTemplate,
|
||||
ContentTreeViewModel => TreeTemplate,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ContentFormControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
Background="Transparent"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringNotEmptyToVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid x:Name="ContentGrid" />
|
||||
</UserControl>
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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 AdaptiveCards.ObjectModel.WinUI3;
|
||||
using AdaptiveCards.Rendering.WinUI3;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ContentFormControl : UserControl
|
||||
{
|
||||
private static readonly AdaptiveCardRenderer _renderer;
|
||||
private ContentFormViewModel? _viewModel;
|
||||
|
||||
// LOAD-BEARING: if you don't hang onto a reference to the RenderedAdaptiveCard
|
||||
// then the GC might clean it up sometime, even while the card is in the UI
|
||||
// tree. If this gets GC'd, then it'll revoke our Action handler, and the
|
||||
// form will do seemingly nothing.
|
||||
private RenderedAdaptiveCard? _renderedCard;
|
||||
|
||||
public ContentFormViewModel? ViewModel { get => _viewModel; set => AttachViewModel(value); }
|
||||
|
||||
static ContentFormControl()
|
||||
{
|
||||
_renderer = new AdaptiveCardRenderer();
|
||||
}
|
||||
|
||||
public ContentFormControl()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
var lightTheme = ActualTheme == Microsoft.UI.Xaml.ElementTheme.Light;
|
||||
_renderer.HostConfig = lightTheme ? AdaptiveCardsConfig.Light : AdaptiveCardsConfig.Dark;
|
||||
|
||||
// TODO in the future, we should handle ActualThemeChanged and replace
|
||||
// our rendered card with one for that theme. But today is not that day
|
||||
}
|
||||
|
||||
private void AttachViewModel(ContentFormViewModel? vm)
|
||||
{
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
||||
}
|
||||
|
||||
_viewModel = vm;
|
||||
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
|
||||
var c = _viewModel.Card;
|
||||
if (c != null)
|
||||
{
|
||||
DisplayCard(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.PropertyName == nameof(ViewModel.Card))
|
||||
{
|
||||
var c = ViewModel.Card;
|
||||
if (c != null)
|
||||
{
|
||||
DisplayCard(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisplayCard(AdaptiveCardParseResult result)
|
||||
{
|
||||
_renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard);
|
||||
ContentGrid.Children.Clear();
|
||||
ContentGrid.Children.Add(_renderedCard.FrameworkElement);
|
||||
_renderedCard.Action += Rendered_Action;
|
||||
}
|
||||
|
||||
private void Rendered_Action(RenderedAdaptiveCard sender, AdaptiveActionEventArgs args) =>
|
||||
ViewModel?.HandleSubmit(args.Action, args.Inputs.AsJson());
|
||||
}
|
||||
138
src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml
Normal file
138
src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml
Normal file
@@ -0,0 +1,138 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.CmdPal.UI.ContentPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:Interactions="using:Microsoft.Xaml.Interactions.Core"
|
||||
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
|
||||
xmlns:cmdPalControls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
Background="Transparent"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<ResourceDictionary>
|
||||
<StackLayout x:Name="VerticalStackLayout" Orientation="Vertical" Spacing="8"/>
|
||||
|
||||
<cmdpalUI:ContentTemplateSelector
|
||||
x:Key="ContentTemplateSelector"
|
||||
FormTemplate="{StaticResource FormContentTemplate}"
|
||||
MarkdownTemplate="{StaticResource MarkdownContentTemplate}"
|
||||
TreeTemplate="{StaticResource TreeContentTemplate}"
|
||||
/>
|
||||
<cmdpalUI:ContentTemplateSelector
|
||||
x:Key="NestedContentTemplateSelector"
|
||||
FormTemplate="{StaticResource NestedFormContentTemplate}"
|
||||
MarkdownTemplate="{StaticResource NestedMarkdownContentTemplate}"
|
||||
TreeTemplate="{StaticResource TreeContentTemplate}"
|
||||
/>
|
||||
|
||||
<DataTemplate x:Key="FormContentTemplate" x:DataType="viewmodels:ContentFormViewModel">
|
||||
<Grid
|
||||
Margin="0,4,4,4"
|
||||
Padding="12,8,8,8"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1,1,1,2"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}">
|
||||
<cmdPalControls:ContentFormControl ViewModel="{x:Bind}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="MarkdownContentTemplate" x:DataType="viewmodels:ContentMarkdownViewModel">
|
||||
<Grid
|
||||
Margin="0,4,4,4"
|
||||
Padding="12,8,8,8"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1,1,1,2"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}">
|
||||
<toolkit:MarkdownTextBlock
|
||||
Background="Transparent"
|
||||
Header3FontSize="12"
|
||||
Header3FontWeight="Normal"
|
||||
Header3Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
IsTextSelectionEnabled="True"
|
||||
Text="{x:Bind Body, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="NestedFormContentTemplate" x:DataType="viewmodels:ContentFormViewModel">
|
||||
<Grid>
|
||||
<cmdPalControls:ContentFormControl ViewModel="{x:Bind}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="NestedMarkdownContentTemplate" x:DataType="viewmodels:ContentMarkdownViewModel">
|
||||
<Grid >
|
||||
<toolkit:MarkdownTextBlock
|
||||
Background="Transparent"
|
||||
Header3FontSize="12"
|
||||
Header3FontWeight="Normal"
|
||||
Header3Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
IsTextSelectionEnabled="True"
|
||||
Text="{x:Bind Body, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="TreeContentTemplate" x:DataType="viewmodels:ContentTreeViewModel">
|
||||
<StackPanel
|
||||
Orientation="Vertical"
|
||||
Margin="0,4,4,4"
|
||||
Padding="12,8,8,8"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1,1,1,2"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}">
|
||||
<ItemsRepeater
|
||||
Margin="8"
|
||||
VerticalAlignment="Stretch"
|
||||
Layout="{StaticResource VerticalStackLayout}"
|
||||
ItemsSource="{x:Bind StupidGames, Mode=OneWay}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<cmdpalUI:ContentTemplateSelector
|
||||
FormTemplate="{StaticResource NestedFormContentTemplate}"
|
||||
MarkdownTemplate="{StaticResource NestedMarkdownContentTemplate}"
|
||||
TreeTemplate="{StaticResource TreeContentTemplate}"
|
||||
/>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
|
||||
<ItemsRepeater
|
||||
Margin="8"
|
||||
VerticalAlignment="Stretch"
|
||||
Visibility="{x:Bind HasChildren, Mode=OneWay}"
|
||||
Layout="{StaticResource VerticalStackLayout}"
|
||||
ItemsSource="{x:Bind Children, Mode=OneWay}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<cmdpalUI:ContentTemplateSelector
|
||||
FormTemplate="{StaticResource FormContentTemplate}"
|
||||
MarkdownTemplate="{StaticResource MarkdownContentTemplate}"
|
||||
TreeTemplate="{StaticResource TreeContentTemplate}"
|
||||
/>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Page.Resources>
|
||||
|
||||
<ScrollView VerticalAlignment="Top" VerticalScrollMode="Enabled">
|
||||
<ItemsRepeater
|
||||
Margin="8"
|
||||
VerticalAlignment="Stretch"
|
||||
Layout="{StaticResource VerticalStackLayout}"
|
||||
ItemTemplate="{StaticResource ContentTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.Content, Mode=OneWay}">
|
||||
</ItemsRepeater>
|
||||
</ScrollView>
|
||||
</Page>
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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.UI.ViewModels;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class ContentPage : Page
|
||||
{
|
||||
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
public ContentPageViewModel? ViewModel
|
||||
{
|
||||
get => (ContentPageViewModel?)GetValue(ViewModelProperty);
|
||||
set => SetValue(ViewModelProperty, value);
|
||||
}
|
||||
|
||||
// Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc...
|
||||
public static readonly DependencyProperty ViewModelProperty =
|
||||
DependencyProperty.Register(nameof(ViewModel), typeof(ContentPageViewModel), typeof(FormsPage), new PropertyMetadata(null));
|
||||
|
||||
public ContentPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is ContentPageViewModel vm)
|
||||
{
|
||||
ViewModel = vm;
|
||||
}
|
||||
|
||||
base.OnNavigatedTo(e);
|
||||
}
|
||||
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) => base.OnNavigatingFrom(e);
|
||||
}
|
||||
@@ -2,10 +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 CommunityToolkit.Common;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
@@ -132,6 +132,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
},
|
||||
IFormPage formsPage => new FormsPageViewModel(formsPage, _mainTaskScheduler, host),
|
||||
IMarkdownPage markdownPage => new MarkdownPageViewModel(markdownPage, _mainTaskScheduler, host),
|
||||
IContentPage contentPage => new ContentPageViewModel(contentPage, _mainTaskScheduler, host),
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
@@ -145,6 +146,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
IListPage => typeof(ListPage),
|
||||
IFormPage => typeof(FormsPage),
|
||||
IMarkdownPage => typeof(MarkdownPage),
|
||||
IContentPage => typeof(ContentPage),
|
||||
_ => throw new NotSupportedException(),
|
||||
},
|
||||
pageViewModel,
|
||||
@@ -165,8 +167,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
else if (command is IInvokableCommand invokable)
|
||||
{
|
||||
// TODO Handle results
|
||||
var result = invokable.Invoke();
|
||||
var result = invokable.Invoke(message.Context);
|
||||
HandleCommandResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +48,7 @@ public sealed partial class MainPage : Microsoft.UI.Xaml.Controls.Page
|
||||
RootFrame.Navigate(typeof(ListPage), rootListVm, new DrillInNavigationTransitionInfo());
|
||||
}
|
||||
|
||||
private void ExtensionService_OnExtensionsChanged(object? sender, EventArgs e)
|
||||
{
|
||||
_ = LoadAllCommands();
|
||||
}
|
||||
private void ExtensionService_OnExtensionsChanged(object? sender, EventArgs e) => _ = LoadAllCommands();
|
||||
|
||||
private void HackyBadClearFilter()
|
||||
{
|
||||
@@ -116,7 +113,7 @@ public sealed partial class MainPage : Microsoft.UI.Xaml.Controls.Page
|
||||
TryAllowForeground(command);
|
||||
if (command is IInvokableCommand invokable)
|
||||
{
|
||||
HandleResult(invokable.Invoke());
|
||||
HandleResult(invokable.Invoke(null));
|
||||
return;
|
||||
}
|
||||
else if (command is IListPage listPage)
|
||||
@@ -236,10 +233,7 @@ public sealed partial class MainPage : Microsoft.UI.Xaml.Controls.Page
|
||||
}
|
||||
}
|
||||
|
||||
private void RequestGoHomeHandler(object sender, object args)
|
||||
{
|
||||
DoGoHome();
|
||||
}
|
||||
private void RequestGoHomeHandler(object sender, object args) => DoGoHome();
|
||||
|
||||
private void DoGoHome()
|
||||
{
|
||||
@@ -254,10 +248,7 @@ public sealed partial class MainPage : Microsoft.UI.Xaml.Controls.Page
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendLog(string message)
|
||||
{
|
||||
_log += message + "\n";
|
||||
}
|
||||
private void AppendLog(string message) => _log += message + "\n";
|
||||
|
||||
private async Task LoadExtensions()
|
||||
{
|
||||
|
||||
@@ -13,6 +13,10 @@ foreach ($item in $json.children) {
|
||||
# Each line that starts with `runtimeclass` or `interface` should be prefixed with the contract attribute
|
||||
$code = $code -replace "(?m)^(runtimeclass|interface) ", "[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]`n`$1 "
|
||||
|
||||
# if there are two [contract] attributes, remove the second one
|
||||
$code = $code -replace "(?m)^\[contract\(Microsoft.CmdPal.Extensions.ExtensionsContract, ([0-9]+)\)\]\n\[contract\(Microsoft.CmdPal.Extensions.ExtensionsContract, 1\)\]", "[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, `$1)]"
|
||||
|
||||
|
||||
# all the lines that start with `(whitespace)async (T)` should be translated to `IAsyncOperation<T>`
|
||||
$code = $code -replace "(?m)^(\s*)async\s+(void)\s+([A-Za-z0-9_]+)\s*\(", "`$1Windows.Foundation.IAsyncAction `$3("
|
||||
$code = $code -replace "(?m)^(\s*)async\s+([A-Za-z0-9_<>]+)\s+([A-Za-z0-9_]+)\s*\(", "`$1Windows.Foundation.IAsyncOperation<`$2> `$3("
|
||||
@@ -33,6 +37,10 @@ foreach ($item in $json.children) {
|
||||
# Each line that starts with `runtimeclass` or `interface` should be prefixed with the contract attribute
|
||||
$code = $code -replace "(?m)^(runtimeclass|interface) ", "[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]`n`$1 "
|
||||
|
||||
# if there are two [contract] attributes, remove the second one
|
||||
$code = $code -replace "(?m)^\[contract\(Microsoft.CmdPal.Extensions.ExtensionsContract, ([0-9]+)\)\]\n\[contract\(Microsoft.CmdPal.Extensions.ExtensionsContract, 1\)\]", "[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, `$1)]"
|
||||
|
||||
|
||||
# all the lines that start with `(whitespace)async (T)` should be translated to `IAsyncOperation<T>`
|
||||
$code = $code -replace "(?m)^(\s*)async\s+(void)\s+([A-Za-z0-9_]+)\s*\(", "`$1Windows.Foundation.IAsyncAction `$3("
|
||||
$code = $code -replace "(?m)^(\s*)async\s+([A-Za-z0-9_<>]+)\s+([A-Za-z0-9_]+)\s*\(", "`$1Windows.Foundation.IAsyncOperation<`$2> `$3("
|
||||
@@ -72,7 +80,7 @@ namespace Microsoft.CmdPal.Extensions
|
||||
String Icon { get; };
|
||||
Windows.Storage.Streams.IRandomAccessStreamReference Data { get; };
|
||||
};
|
||||
|
||||
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
runtimeclass IconInfo {
|
||||
IconInfo(String iconString);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
author: Mike Griese
|
||||
created on: 2024-07-19
|
||||
last updated: 2025-01-08
|
||||
last updated: 2025-02-03
|
||||
issue id: n/a
|
||||
---
|
||||
|
||||
@@ -57,9 +57,12 @@ functionality.
|
||||
- [Filtering the list](#filtering-the-list)
|
||||
- [Markdown Pages](#markdown-pages)
|
||||
- [Form Pages](#form-pages)
|
||||
- [Content Pages](#content-pages)
|
||||
- [Markdown Content](#markdown-content)
|
||||
- [Form Content](#form-content)
|
||||
- [Other types](#other-types)
|
||||
- [`ContextItem`s](#contextitems)
|
||||
- [Icons - `IconInfo` and `IconData`](#icons---iconinfo-and-icondatatype)
|
||||
- [Icons - `IconInfo` and `IconData`](#icons---iconinfo-and-icondata)
|
||||
- [`OptionalColor`](#optionalcolor)
|
||||
- [`Details`](#details)
|
||||
- [`INotifyPropChanged`](#inotifypropchanged)
|
||||
@@ -80,6 +83,8 @@ functionality.
|
||||
- [URI activation](#uri-activation)
|
||||
- [Custom "empty list" messages](#custom-empty-list-messages)
|
||||
- [Footnotes](#footnotes)
|
||||
- [Generating the `.idl`](#generating-the-idl)
|
||||
- [Adding APIs](#adding-apis)
|
||||
|
||||
|
||||
## Background
|
||||
@@ -256,7 +261,7 @@ commands won't change over time. These extensions can be cached to save
|
||||
resources.
|
||||
|
||||
Command providers can opt out of this behavior by setting `Frozen=false` in
|
||||
their extension. We'll call these extensions "**fresh, never frozen**".
|
||||
their extension. We'll call these extensions "**fresh, never frozen**".
|
||||
|
||||
As some examples:
|
||||
* The "Hacker News" extension, only has a single top-level action. Once we load
|
||||
@@ -265,7 +270,7 @@ As some examples:
|
||||
* Similarly for something like the GitHub extension - it's got multiple
|
||||
top-level commands (My issues, Issue search, Repo search, etc), but these
|
||||
top-level commands never change. This is a **frozen** extension.
|
||||
* The "Quick Links" extension has a dynamic list of top-level actions.
|
||||
* The "Quick Links" extension has a dynamic list of top-level actions.
|
||||
This is a **fresh** extension.[^3]
|
||||
* The "Media Controls" extension only has a single top-level action, but it
|
||||
needs to be running to be able to update it's title and icon. So we can't just
|
||||
@@ -311,18 +316,18 @@ The structure of the data DevPal caches will look something like the following:
|
||||
```
|
||||
|
||||
In this data you can see:
|
||||
* We cache some basic info about each extension we've seen. This includes
|
||||
* the Package Family Name (a unique identifier per-app package),
|
||||
* the COM CLSID for that extension,
|
||||
* We cache some basic info about each extension we've seen. This includes
|
||||
* the Package Family Name (a unique identifier per-app package),
|
||||
* the COM CLSID for that extension,
|
||||
* the display name for that extension,
|
||||
* and if that extension is frozen or not.
|
||||
* and if that extension is frozen or not.
|
||||
* We also cache the list of top-level commands for that extension. We'll store
|
||||
the basic amount of info we need to recreate that command in the top-level
|
||||
list.
|
||||
|
||||
On a cold launch, DevPal will do the following:
|
||||
|
||||
1. SLOW: First we start up WASDK and XAML. Unavoidable cost.
|
||||
1. SLOW: First we start up WASDK and XAML. Unavoidable cost.
|
||||
2. FAST: We load builtin extensions. These are just extensions in DLLs, so
|
||||
there's nothing to it.
|
||||
3. FAST: We load our cache of extensions from disk, and note which are frozen vs fresh
|
||||
@@ -342,10 +347,10 @@ On a cold launch, DevPal will do the following:
|
||||
5. SLOW: We open the package catalog for more commands
|
||||
* Extensions that we've seen before in our cache:
|
||||
* If it's fresh, we'll start it, and fill in commands from `TopLevelCommands` into the palette
|
||||
* If it's frozen, we'll leave it be. We've already got stubs for it.
|
||||
* Extensions we've never seen before:
|
||||
* If it's frozen, we'll leave it be. We've already got stubs for it.
|
||||
* Extensions we've never seen before:
|
||||
* Start it up.
|
||||
* Check if it's fresh or frozen.
|
||||
* Check if it's fresh or frozen.
|
||||
* Call `TopLevelCommands`, and put all of them in the list
|
||||
* Create a extension cache entry for that app.
|
||||
* If the provider is frozen: we can actually release the
|
||||
@@ -355,9 +360,9 @@ On a cold launch, DevPal will do the following:
|
||||
6. We start a package catalog change watcher to be notified by the OS for
|
||||
changes to the list of installed extensions
|
||||
|
||||
After 1, we can display the UI. It won't have any commands though, so maybe we should wait.
|
||||
After 1, we can display the UI. It won't have any commands though, so maybe we should wait.
|
||||
After 2, we'd have some commands, but nothing from extensions
|
||||
After 4, the palette is ready to be used, with all the frozen extension commands. This is probably good enough for most use cases.
|
||||
After 4, the palette is ready to be used, with all the frozen extension commands. This is probably good enough for most use cases.
|
||||
|
||||
Most of the time, when the user "launches" devPal, we won't run through this
|
||||
whole process. The slowest part of startup is standing up WASDK and WinUI. After
|
||||
@@ -383,7 +388,7 @@ command), we need to quickly load that app and get the command for it.
|
||||
3. Check if the extension is already in the warm extension cache. If it is, we
|
||||
recently reheated a command from this provider. We can skip step 4 and go
|
||||
straight to step 5
|
||||
4. Use the CLSID from the cache to `CoCreateInstance` this extension, and get its `ICommandProvider`.
|
||||
4. Use the CLSID from the cache to `CoCreateInstance` this extension, and get its `ICommandProvider`.
|
||||
* If that fails: display an error message.
|
||||
5. Try to load the command from the provider. This is done in two steps:
|
||||
1. If the cached command had an `id`, try to look up the command with
|
||||
@@ -393,7 +398,7 @@ command), we need to quickly load that app and get the command for it.
|
||||
null): all `TopLevelItems` on that `CommandProvider`.
|
||||
* Search through all the returned commands with the same `id` or
|
||||
`icon/title/subtitle/name`, and return that one.
|
||||
6. If we found the command from the provider, navigate to it or invoke it.
|
||||
6. If we found the command from the provider, navigate to it or invoke it.
|
||||
|
||||
##### Microwaved commands
|
||||
|
||||
@@ -409,7 +414,7 @@ keep warm at a given time. We'll probably also want to offer an option like
|
||||
the memory usage as much.
|
||||
|
||||
> [WARNING!]
|
||||
>
|
||||
>
|
||||
> If your command provider returns a `IFallbackCommandItem`s from
|
||||
> `FallbackCommands`, and that provider is marked `frozen`, DevPal will always
|
||||
> treat your provider as "fresh". Otherwise, devpal wouldn't be able to call
|
||||
@@ -428,7 +433,7 @@ that limit of recent commands, we'll release our reference to the COM object for
|
||||
that extension, and re-mark commands from it as "stubs". Upon the release of
|
||||
that reference, the extension is free to clean itself up. For extensions that
|
||||
use the helpers library, they can override `CommandProvider.Dispose` to do
|
||||
cleanup in there.
|
||||
cleanup in there.
|
||||
|
||||
## Installing extensions
|
||||
|
||||
@@ -490,7 +495,7 @@ anything that a 1p built-in can do.
|
||||
The SDK for DevPal is split into two namespaces:
|
||||
* `Microsoft.Windows.Run` - This namespace contains the interfaces that
|
||||
developers will implement to create extensions for DevPal.
|
||||
* `Microsoft.Windows.Run.Extensions` - This namespace contains helper classes
|
||||
* `Microsoft.CmdPal.Extensions.Helpers` - This namespace contains helper classes
|
||||
that developers can use to make creating extensions easier.
|
||||
|
||||
The first is highly abstract, and gives developers total control over the
|
||||
@@ -540,9 +545,11 @@ enum CommandResultKind {
|
||||
Dismiss, // Reset the palette to the main page and dismiss
|
||||
GoHome, // Go back to the main page, but keep it open
|
||||
GoBack, // Go back one level
|
||||
Hide, // Keep this page open, but hide the palette.
|
||||
Hide, // Keep this page open, but hide the palette.
|
||||
KeepOpen, // Do nothing.
|
||||
GoToPage, // Go to another page. GoToPageArgs will tell you where.
|
||||
ShowToast, // Display a transient message to the user
|
||||
Confirm, // Display a confirmation dialog
|
||||
};
|
||||
|
||||
enum NavigationMode {
|
||||
@@ -561,13 +568,24 @@ interface IGoToPageArgs requires ICommandResultArgs{
|
||||
String PageId { get; };
|
||||
NavigationMode NavigationMode { get; };
|
||||
}
|
||||
interface IToastArgs requires ICommandResultArgs{
|
||||
String Message { get; };
|
||||
ICommandResult Result { get; };
|
||||
}
|
||||
interface IConfirmationArgs requires ICommandResultArgs{
|
||||
String Title { get; };
|
||||
String Description { get; };
|
||||
ICommand PrimaryCommand { get; };
|
||||
Boolean IsPrimaryCommandCritical { get; };
|
||||
}
|
||||
|
||||
// This is a "leaf" of the UI. This is something that can be "done" by the user.
|
||||
// * A ListPage
|
||||
// * the MoreCommands flyout of for a ListItem or a MarkdownPage
|
||||
interface IInvokableCommand requires ICommand {
|
||||
ICommandResult Invoke();
|
||||
ICommandResult Invoke(Object sender);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If a developer wants to add a simple action to DevPal, they can create a
|
||||
@@ -577,7 +595,7 @@ method will be called when the user selects the action in DevPal.
|
||||
As a simple example[^1]:
|
||||
|
||||
```cs
|
||||
class HackerNewsAction : Microsoft.Windows.Run.Extensions.InvokableCommand {
|
||||
class HackerNewsAction : Microsoft.CmdPal.Extensions.Helpers.InvokableCommand {
|
||||
public class HackerNewsAction()
|
||||
{
|
||||
Name = "Hacker News";
|
||||
@@ -603,6 +621,31 @@ The `Id` property is optional. This can be set but the extension author to
|
||||
support more efficient command lookup in
|
||||
[`ICommandProvider.GetCommand()`, below](#getcommand).
|
||||
|
||||
When `Invoke` is called, the host app will pass in a `sender` object that
|
||||
represents the context of where that command was invoked from. This can be
|
||||
different types depending on where the command is being used:
|
||||
|
||||
* `TopLevelCommands` (and fallbacks)
|
||||
* Sender is the `ICommandItem` for the top-level command that was invoked
|
||||
* `IListPage.GetItems`
|
||||
* Sender is the `IListItem` for the list item selected for that command
|
||||
* `ICommandItem.MoreCommands` (context menus)
|
||||
* Sender is the `IListItem` which the command was attached to for a list page, or
|
||||
* the `ICommandItem` of the top-level command (if this is a context item on a top level command)
|
||||
* `IContentPage.Commands`
|
||||
* Sender is the `IContentPage` itself
|
||||
|
||||
The helpers library also exposes a `Invoke()` method on `InvokableCommand` which
|
||||
takes no parameters, as a convenience for developers who don't need the `sender`
|
||||
object.
|
||||
|
||||
Using the `sender` parameter can be useful for big lists of items where the
|
||||
actionable information for each item is practically the same. Consider a big
|
||||
list of links. An extension developer can implement this as a single
|
||||
`IInvokableCommand` that opens a URL based on the `sender` object passed in.
|
||||
Then each list item would store the URL to open and the title of the link. This
|
||||
creates less overhead for the extension and host to communicate.
|
||||
|
||||
#### Results
|
||||
|
||||
Commands can return a `CommandResult` to indicate what DevPal should do after
|
||||
@@ -638,9 +681,29 @@ Use cases for each `CommandResultKind`:
|
||||
stay in its current state.
|
||||
* `GoToPage` - Navigate to a different page in DevPal. The `GoToPageArgs`
|
||||
will specify which page to navigate to.
|
||||
* [TODO!]: Do we actually need this, now that all the commands can be pages?
|
||||
* Does this satisfy "I want to pop the stack, but then push something else
|
||||
onto the stack"? Versus the default which is just "add this to the stack"?
|
||||
* `Push`: The new page gets added to the current navigation stack. Going back
|
||||
from the requested page will take you to the current page.
|
||||
* `GoBack`: Go back one level, then navigate to the page. Going back from the
|
||||
requested page will take you to the page before the current page.
|
||||
* `GoHome`: Clear the back stack, then navigate to the page. Going back from
|
||||
the requested page will take you to the home page (the L0).
|
||||
* `ShowToast` - Display a transient desktop-level message to the user. This is
|
||||
especially useful for displaying confirmation that an action took place, when
|
||||
the palette will be closed. Consider the `CopyTextCommand` in the helpers -
|
||||
this command will show a toast with the text "Copied to clipboard", then
|
||||
dismiss the palette.
|
||||
* Once the message is displayed, the palette will then react to the `Result`.
|
||||
In the helpers library, the `ToastArgs`'s default `Result` value is
|
||||
`Dismiss`.
|
||||
* Only one toast can be displayed at a time. If a new toast is requested
|
||||
before the previous one is dismissed, the new toast will replace the old
|
||||
one. This includes if the `Result` of one `IToastArgs` is another
|
||||
`IToastArgs`.
|
||||
* `Confirm`: Display a confirmation dialog to the user. This is useful for
|
||||
actions that are destructive or irreversible. The `ConfirmationArgs` will
|
||||
specify the title, and description for the dialog. The primary button of the
|
||||
dialog will activate the `Command`. If `IsPrimaryCommandCritical` is `true`,
|
||||
the primary button will be red, indicating that it is a destructive action.
|
||||
|
||||
### Pages
|
||||
|
||||
@@ -654,7 +717,7 @@ information that the host application will then use to render the page.
|
||||
interface IPage requires ICommand {
|
||||
String Title { get; };
|
||||
Boolean IsLoading { get; };
|
||||
|
||||
|
||||
OptionalColor AccentColor { get; };
|
||||
}
|
||||
```
|
||||
@@ -663,9 +726,8 @@ When a user selects an action that implements `IPage`, DevPal will navigate
|
||||
to that page, pushing it onto the UI stack.
|
||||
|
||||
Pages can be one of several types, each detailed below:
|
||||
* [List](#List)
|
||||
* [Markdown](#Markdown)
|
||||
* [Form](#Form)
|
||||
* [List](#List-Pages)
|
||||
* [Content](#Content-Pages)
|
||||
|
||||
If a page returns a null or empty `Title`, DevPal will display the `Name` of the
|
||||
`ICommand` instead.
|
||||
@@ -706,7 +768,7 @@ Lists can be either "static" or "dynamic":
|
||||
results - it's the extension's responsibility to filter them.
|
||||
* Ex: The GitHub extension may want to allow the user to type `is:issue
|
||||
is:open`, then return a list of open issues, without string matching on
|
||||
the text.
|
||||
the text.
|
||||
|
||||
|
||||
```csharp
|
||||
@@ -719,7 +781,7 @@ interface ICommandItem requires INotifyPropChanged {
|
||||
IconInfo Icon{ get; };
|
||||
String Title{ get; };
|
||||
String Subtitle{ get; };
|
||||
}
|
||||
}
|
||||
|
||||
interface ICommandContextItem requires ICommandItem, IContextItem {
|
||||
Boolean IsCritical { get; }; // READ: "make this red"
|
||||
@@ -741,7 +803,7 @@ interface IGridProperties {
|
||||
}
|
||||
|
||||
interface IListPage requires IPage, INotifyItemsChanged {
|
||||
// DevPal will be responsible for filtering the list of items, unless the
|
||||
// DevPal will be responsible for filtering the list of items, unless the
|
||||
// class implements IDynamicListPage
|
||||
String SearchText { get; };
|
||||
String PlaceholderText { get; };
|
||||
@@ -751,7 +813,7 @@ interface IListPage requires IPage, INotifyItemsChanged {
|
||||
Boolean HasMoreItems { get; };
|
||||
ICommandItem EmptyContent { get; };
|
||||
|
||||
IListItem[] GetItems();
|
||||
IListItem[] GetItems();
|
||||
void LoadMore();
|
||||
}
|
||||
|
||||
@@ -767,7 +829,7 @@ Lists are comprised of a collection of `IListItems`.
|
||||
|
||||

|
||||
|
||||
> NOTE: The above diagram is from before Nov 2024. It doesn't properly include the relationship between `ICommandItems` and list items.
|
||||
> NOTE: The above diagram is from before Nov 2024. It doesn't properly include the relationship between `ICommandItems` and list items.
|
||||
> A more up-to-date explainer of the elements of the UI can be found in
|
||||
> ["Rendering of ICommandItems in Lists and Menus"](#rendering-of-icommanditems-in-lists-and-menus)
|
||||
|
||||
@@ -787,13 +849,13 @@ the commands for the currently selected item.
|
||||
The elements of a ListPage (`IListItem`s) and the context menu
|
||||
(`ICommandContextItem`) both share the same base type. Basically, they're both a
|
||||
list of things which have:
|
||||
* A `ICommand` to invoke or navigate to.
|
||||
* a `Title` which might replace their `Command`'s `Name`,
|
||||
* an `Icon` which might replace their `Command`'s `Icon`,
|
||||
* A `ICommand` to invoke or navigate to.
|
||||
* a `Title` which might replace their `Command`'s `Name`,
|
||||
* an `Icon` which might replace their `Command`'s `Icon`,
|
||||
* A `Subtitle`, which is visible on the list, and a _tooltip_ for a context menu
|
||||
* They might also have `MoreCommands`:
|
||||
* For a `IListItem`, this is the context menu.
|
||||
* For a ContextItem in the context menu, this creates a sub-context menu.
|
||||
* For a `IListItem`, this is the context menu.
|
||||
* For a ContextItem in the context menu, this creates a sub-context menu.
|
||||
|
||||
For more details on the structure of the `MoreCommands` property, see the
|
||||
[`ContextItem`s](#contextitems) section below.
|
||||
@@ -917,21 +979,21 @@ class NewsPost {
|
||||
string Poster;
|
||||
int Points;
|
||||
}
|
||||
class LinkAction(NewsPost post) : Microsoft.Windows.Run.Extensions.InvokableCommand {
|
||||
class LinkAction(NewsPost post) : Microsoft.CmdPal.Extensions.Helpers.InvokableCommand {
|
||||
public string Name => "Open link";
|
||||
public ActionResult Invoke() {
|
||||
public CommandResult Invoke() {
|
||||
Process.Start(new ProcessStartInfo(post.Url) { UseShellExecute = true });
|
||||
return ActionResult.KeepOpen;
|
||||
return CommandResult.KeepOpen;
|
||||
}
|
||||
}
|
||||
class CommentAction(NewsPost post) : Microsoft.Windows.Run.Extensions.InvokableCommand {
|
||||
class CommentAction(NewsPost post) : Microsoft.CmdPal.Extensions.Helpers.InvokableCommand {
|
||||
public string Name => "Open comments";
|
||||
public ActionResult Invoke() {
|
||||
public CommandResult Invoke() {
|
||||
Process.Start(new ProcessStartInfo(post.CommentsUrl) { UseShellExecute = true });
|
||||
return ActionResult.KeepOpen;
|
||||
return CommandResult.KeepOpen;
|
||||
}
|
||||
}
|
||||
class NewsListItem(NewsPost post) : Microsoft.Windows.Run.Extensions.ListItem {
|
||||
class NewsListItem(NewsPost post) : Microsoft.CmdPal.Extensions.Helpers.ListItem {
|
||||
public string Title => post.Title;
|
||||
public string Subtitle => post.Poster;
|
||||
public IContextItem[] Commands => [
|
||||
@@ -940,7 +1002,7 @@ class NewsListItem(NewsPost post) : Microsoft.Windows.Run.Extensions.ListItem {
|
||||
];
|
||||
public ITag[] Tags => [ new Tag(){ Text=post.Points } ];
|
||||
}
|
||||
class HackerNewsPage: Microsoft.Windows.Run.Extensions.ListPage {
|
||||
class HackerNewsPage: Microsoft.CmdPal.Extensions.Helpers.ListPage {
|
||||
public bool Loading => true;
|
||||
IListItem[] GetItems() {
|
||||
List<NewsItem> items = /* do some RSS feed stuff */;
|
||||
@@ -987,16 +1049,16 @@ Here's a breakdown of how a dynamic list responds to the CmdPal. In this
|
||||
example, we'll use a hypothetical GitHub issue search extension, which allows
|
||||
the user to type a query and get a list of issues back.
|
||||
|
||||
1. CmdPal loads the `ListPage` from the extension.
|
||||
1. CmdPal loads the `ListPage` from the extension.
|
||||
2. It is a `IDynamicListPage`, so the command palette knows not to do any
|
||||
host-side filtering.
|
||||
3. CmdPal reads the `SearchText` from the ListPage
|
||||
3. CmdPal reads the `SearchText` from the ListPage
|
||||
- it returns `is:issue is:open` as initial text
|
||||
4. CmdPal reads the `HasMoreItems` from the ListPage
|
||||
4. CmdPal reads the `HasMoreItems` from the ListPage
|
||||
- it returns `true`
|
||||
5. CmdPal calls `GetItems()`
|
||||
- the extension returns the first 25 items that match the query.
|
||||
6. User scrolls the page to the bottom
|
||||
5. CmdPal calls `GetItems()`
|
||||
- the extension returns the first 25 items that match the query.
|
||||
6. User scrolls the page to the bottom
|
||||
- CmdPal calls `GetMore` on the ListPage, to let it know it should start
|
||||
fetching more results
|
||||
7. The extension raises a `ItemsChanged(40)`, to indicate that it now has 40 items
|
||||
@@ -1089,7 +1151,7 @@ applied to top-level `IListItem`s:
|
||||
* We won't display any context menu commands for these entries
|
||||
* We won't display the Details for these entries (nor the context action to show
|
||||
details)
|
||||
* Icons which are a `IRandomAccessStream` will not work as expected.
|
||||
* Icons which are a `IRandomAccessStream` will not work as expected.
|
||||
* If you create a top-level `IListItem` that implements `IFallbackHandler`,
|
||||
DevPal will treat your `ICommandProvider` as fresh, never frozen, regardless
|
||||
of the value of `Frozen` you set.
|
||||
@@ -1100,12 +1162,6 @@ on nested pages will all work exactly as expected. -->
|
||||
|
||||
#### Markdown Pages
|
||||
|
||||
This is a page that displays a block of markdown text. This is useful for
|
||||
showing a lot of information in a small space. Markdown provides a rich set of
|
||||
simple formatting options.
|
||||
|
||||

|
||||
|
||||
```csharp
|
||||
interface IMarkdownPage requires IPage {
|
||||
String[] Bodies(); // TODO! should this be an IBody, so we can make it observable?
|
||||
@@ -1114,75 +1170,8 @@ interface IMarkdownPage requires IPage {
|
||||
}
|
||||
```
|
||||
|
||||
A markdown page may also have a `Details` property, which will be displayed in
|
||||
the same way as the details for a list item. This is useful for showing
|
||||
additional information about the page, like a description, a preview of a file,
|
||||
or a link to more information.
|
||||
|
||||
Similar to the `List` page, the `Commands` property is a list of commands that the
|
||||
user can take on the page. These are the commands that will be shown in the "More
|
||||
actions" flyout. Unlike the `List` page, the `Commands` property is not
|
||||
associated with any specific item on the page, rather, these commands are global
|
||||
to the page itself.
|
||||
|
||||
An example markdown page for an issue on GitHub:
|
||||
|
||||
```cs
|
||||
class GitHubIssue {
|
||||
string Title;
|
||||
string Url;
|
||||
string Body;
|
||||
string Author;
|
||||
string[] Tags;
|
||||
string[] AssignedTo;
|
||||
}
|
||||
class OpenLinkAction(GitHubIssue issue) : Microsoft.Windows.Run.Extensions.InvokableCommand {
|
||||
public string Name => "Open";
|
||||
public ActionResult Invoke() {
|
||||
Process.Start(new ProcessStartInfo(issue.Url) { UseShellExecute = true });
|
||||
return ActionResult.KeepOpen;
|
||||
}
|
||||
}
|
||||
class GithubIssuePage(GithubIssue issue): Microsoft.Windows.Run.Extensions.MarkdownPage {
|
||||
public bool Loading => true;
|
||||
public string Body() {
|
||||
issue.Body = /* fetch the body from the API */;
|
||||
this.IsLoading = false;
|
||||
return issue.Body;
|
||||
}
|
||||
public IContextItem[] Commands => [ new CommandContextItem(new OpenLinkAction(issue)) ];
|
||||
public IDetails Details() {
|
||||
return new Details(){
|
||||
Title = "",
|
||||
Body = "",
|
||||
Metadata = [
|
||||
new Microsoft.Windows.Run.Extensions.DetailsTags(){
|
||||
Key = "Author",
|
||||
Tags = new(){ new Tag(){ Text = issue.Author } }
|
||||
},
|
||||
new Microsoft.Windows.Run.Extensions.DetailsTags(){
|
||||
Key = "Assigned To",
|
||||
Tags = issue.AssignedTo.Select((user) => new Tag(){ Text = user }).ToArray()
|
||||
},
|
||||
new Microsoft.Windows.Run.Extensions.DetailsTags(){
|
||||
Key = "Tags",
|
||||
Tags = issue.Tags.Select((tag) => new Tag(){ Text = tag }).ToArray()
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Form Pages
|
||||
|
||||
A form page allows the user to input data to the extension. This is useful for
|
||||
actions that might require additional information from the user. For example:
|
||||
imagine a "Send Teams message" action. This action might require the user to
|
||||
input the message they want to send, and give the user a dropdown to pick the
|
||||
chat to send the message to.
|
||||
|
||||

|
||||
|
||||
```csharp
|
||||
|
||||
@@ -1197,7 +1186,120 @@ interface IFormPage requires IPage {
|
||||
}
|
||||
```
|
||||
|
||||
Form pages are powered by [Adaptive Cards](https://adaptivecards.io/). This
|
||||
|
||||
#### Content Pages
|
||||
|
||||
Content pages are used for extensions that want to display richer content than
|
||||
just a list of commands to the user. These pages are useful for displaying
|
||||
things like documents and forms. You can mix and match different types of
|
||||
content on a single page, and even nest content within other content.
|
||||
|
||||
```csharp
|
||||
[uuid("b64def0f-8911-4afa-8f8f-042bd778d088")]
|
||||
interface IContent requires INotifyPropChanged {
|
||||
}
|
||||
|
||||
interface IFormContent requires IContent {
|
||||
String TemplateJson { get; };
|
||||
String DataJson { get; };
|
||||
String StateJson { get; };
|
||||
ICommandResult SubmitForm(String payload);
|
||||
}
|
||||
|
||||
interface IMarkdownContent requires IContent {
|
||||
String Body { get; };
|
||||
}
|
||||
|
||||
interface ITreeContent requires IContent, INotifyItemsChanged {
|
||||
IContent RootContent { get; };
|
||||
IContent[] GetChildren();
|
||||
}
|
||||
|
||||
interface IContentPage requires IPage, INotifyItemsChanged {
|
||||
IContent[] GetContent();
|
||||
IDetails Details { get; };
|
||||
IContextItem[] Commands { get; };
|
||||
}
|
||||
```
|
||||
|
||||
Content pages may also have a `Details` property, which will be displayed in
|
||||
the same way as the details for a list item. This is useful for showing
|
||||
additional information about the page, like a description, a preview of a file,
|
||||
or a link to more information.
|
||||
|
||||
Similar to the `List` page, the `Commands` property is a list of commands that the
|
||||
user can take on the page. These are the commands that will be shown in the "More
|
||||
actions" flyout. Unlike the `List` page, the `Commands` property is not
|
||||
associated with any specific item on the page, rather, these commands are global
|
||||
to the page itself.
|
||||
|
||||
##### Markdown Content
|
||||
|
||||
This is a block of content that displays text formatted with Markdown. This is
|
||||
useful for showing a lot of information in a small space. Markdown provides a
|
||||
rich set of simple formatting options.
|
||||
|
||||

|
||||
|
||||
|
||||
An example markdown page for an issue on GitHub:
|
||||
|
||||
```cs
|
||||
class GitHubIssue {
|
||||
string Title;
|
||||
string Url;
|
||||
string Body;
|
||||
string Author;
|
||||
string[] Tags;
|
||||
string[] AssignedTo;
|
||||
}
|
||||
class GithubIssuePage: Microsoft.CmdPal.Extensions.Helpers.ContentPage {
|
||||
private readonly MarkdownContent issueBody;
|
||||
public GithubIssuePage(GithubIssue issue)
|
||||
{
|
||||
Commands = [ new CommandContextItem(new Microsoft.CmdPal.Extensions.Helpers.OpenUrlCommand(issue.Url)) ];
|
||||
Details = new Details(){
|
||||
Title = "",
|
||||
Body = "",
|
||||
Metadata = [
|
||||
new Microsoft.CmdPal.Extensions.Helpers.DetailsTags(){
|
||||
Key = "Author",
|
||||
Tags = [new Tag(issue.Author)]
|
||||
},
|
||||
new Microsoft.CmdPal.Extensions.Helpers.DetailsTags(){
|
||||
Key = "Assigned To",
|
||||
Tags = issue.AssignedTo.Select((user) => new Tag(user)).ToArray()
|
||||
},
|
||||
new Microsoft.CmdPal.Extensions.Helpers.DetailsTags(){
|
||||
Key = "Tags",
|
||||
Tags = issue.Tags.Select((tag) => new Tag(tag)).ToArray()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
issueBody = new MarkdownContent(issue.Body);
|
||||
}
|
||||
|
||||
public override IContent[] GetContent() => [issueBody];
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> A real GitHub extension would likely load the issue body asynchronously. In
|
||||
> that case, the page could start a background thread to fetch the content, then
|
||||
> raise the ItemsChanged to signal the host to retrieve the new `IContent`.
|
||||
|
||||
##### Form Content
|
||||
|
||||
Forms allow the user to input data to the extension. This is useful for
|
||||
actions that might require additional information from the user. For example:
|
||||
imagine a "Send Teams message" action. This action might require the user to
|
||||
input the message they want to send, and give the user a dropdown to pick the
|
||||
chat to send the message to.
|
||||
|
||||

|
||||
|
||||
Form content is powered by [Adaptive Cards](https://adaptivecards.io/). This
|
||||
allows extension developers a rich set of controls to use in their forms. Each
|
||||
page can have as many forms as it needs. These forms will be displayed to the
|
||||
user as separate "cards", in the order they are returned by the extension.
|
||||
@@ -1209,7 +1311,6 @@ When the user submits the form, the `SubmitForm` method will be called with the
|
||||
JSON payload of the form. The extension is responsible for parsing this payload
|
||||
and acting on it.
|
||||
|
||||
[TODO!discussion]: Do we want to stick the `Actions` on this page type too? Or does that not make sense?
|
||||
|
||||
### Other types
|
||||
|
||||
@@ -1288,7 +1389,7 @@ struct OptionalColor
|
||||
We also define `OptionalColor` as a helper struct here. Yes, this is also just
|
||||
an `IReference<Color>`. However, `IReference` has some weird ownership semantics
|
||||
that just make it a pain for something as simple as "maybe this color doesn't
|
||||
have a value set".
|
||||
have a value set".
|
||||
|
||||
#### `Details`
|
||||
|
||||
@@ -1317,7 +1418,6 @@ interface ITag {
|
||||
OptionalColor Foreground { get; };
|
||||
OptionalColor Background { get; };
|
||||
String ToolTip { get; };
|
||||
ICommand Command { get; };
|
||||
};
|
||||
|
||||
[uuid("6a6dd345-37a3-4a1e-914d-4f658a4d583d")]
|
||||
@@ -1339,6 +1439,9 @@ interface IDetailsLink requires IDetailsData {
|
||||
Windows.Foundation.Uri Link { get; };
|
||||
String Text { get; };
|
||||
}
|
||||
interface IDetailsCommand requires IDetailsData {
|
||||
ICommand Command { get; };
|
||||
}
|
||||
[uuid("58070392-02bb-4e89-9beb-47ceb8c3d741")]
|
||||
interface IDetailsSeparator requires IDetailsData {}
|
||||
```
|
||||
@@ -1378,7 +1481,7 @@ This is the interface that an extension must implement to provide commands to De
|
||||
|
||||
```csharp
|
||||
interface ICommandSettings {
|
||||
IFormPage SettingsPage { get; };
|
||||
IContentPage SettingsPage { get; };
|
||||
};
|
||||
|
||||
interface IFallbackHandler {
|
||||
@@ -1409,7 +1512,7 @@ interface ICommandProvider requires Windows.Foundation.IClosable, INotifyItemsCh
|
||||
`TopLevelCommands` is the method that DevPal will call to get the list of actions
|
||||
that should be shown when the user opens DevPal. These are the commands that will
|
||||
allow the user to interact with the rest of your extension. They can be simple
|
||||
actions, or they can be pages that the user can navigate to.
|
||||
actions, or they can be pages that the user can navigate to.
|
||||
|
||||
`TopLevelCommands` returns a list of `ICommandItem`s. These are basically just a
|
||||
simpler form of `IListItem`, which can be displayed even as a stub (as described
|
||||
@@ -1421,14 +1524,14 @@ something like an extension that might require the user to login before
|
||||
accessing certain pages within the extension. Command Providers which are
|
||||
`Frozen=true` can also use this event to change their list of cached commands,
|
||||
since the only time an extension can raise this event is when it's already
|
||||
running.
|
||||
running.
|
||||
|
||||
`Id` is only necessary to set if your extension implements multiple providers in
|
||||
the same package identity. This is an uncommon scenario which most developers
|
||||
shouldn't need to worry about. If you do set `Id`, it should be a stable string
|
||||
across package versions. DevPal will use this Id for tracking settings for each
|
||||
provider within a package. Changing this string will result in the user's
|
||||
settings for your extension being lost.
|
||||
settings for your extension being lost.
|
||||
|
||||
#### Fallback commands
|
||||
|
||||
@@ -1462,7 +1565,7 @@ As an example, here's how a developer might implement a fallback action that
|
||||
changes its name to be mOcKiNgCaSe.
|
||||
|
||||
```cs
|
||||
public class SpongebotPage : Microsoft.Windows.Run.Extensions.MarkdownPage, IFallbackHandler
|
||||
public class SpongebotPage : Microsoft.CmdPal.Extensions.Helpers.MarkdownPage, IFallbackHandler
|
||||
{
|
||||
// Name, Icon, IPropertyChanged: all those are defined in the MarkdownPage base class
|
||||
public SpongebotPage()
|
||||
@@ -1496,7 +1599,7 @@ internal sealed class SpongebotCommandsProvider : CommandProvider
|
||||
{
|
||||
public ICommandItem[] TopLevelCommands() => [];
|
||||
public IFallbackCommandItem[] FallbackCommands()
|
||||
{
|
||||
{
|
||||
var spongebotPage = new SpongebotPage();
|
||||
var listItem = new FallbackCommandItem(spongebotPage);
|
||||
// ^ The FallbackCommandItem ctor will automatically set its FallbackHandler to the
|
||||
@@ -1506,7 +1609,7 @@ internal sealed class SpongebotCommandsProvider : CommandProvider
|
||||
}
|
||||
```
|
||||
|
||||
`Microsoft.Windows.Run.Extensions.FallbackCommandItem` in the SDK helpers will automatically set
|
||||
`Microsoft.CmdPal.Extensions.Helpers.FallbackCommandItem` in the SDK helpers will automatically set
|
||||
the `FallbackHandler` property on the `IFallbackCommandItem` to the `Command` it's
|
||||
initialized with, if that command implements `IFallbackHandler`. This allows the
|
||||
action to directly update itself in response to the query. You may also specify
|
||||
@@ -1521,7 +1624,7 @@ If an extension's own list page wants to implement a similar fallback mechanism
|
||||
- it's free to use `IDynamicListPage` to listen for changes to the query and
|
||||
have its own ListItem it updates manually.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> [!IMPORTANT]
|
||||
> If your extension has top-level `FallbackCommandItem`s, then
|
||||
> DevPal will treat your `ICommandProvider` as fresh, never frozen, regardless
|
||||
of the value of `Frozen` you set.
|
||||
@@ -1536,7 +1639,7 @@ that command.
|
||||
For command providers that have multiple top-level commands, this can be a
|
||||
helpful short-circuit. The extension won't need to construct instances of all
|
||||
the `IListItem`s for all its top-level commands. Instead, the extension can just
|
||||
instantiate the requested one.
|
||||
instantiate the requested one.
|
||||
|
||||
##### Settings
|
||||
|
||||
@@ -1585,7 +1688,7 @@ We'll provide default implementations for the following interfaces:
|
||||
This will allow developers to quickly create extensions without having to worry
|
||||
about implementing every part of the interface. You can see that reference
|
||||
implementation in
|
||||
`extensionsdk\Microsoft.Windows.Run.Extensions.Lib\DefaultClasses.cs`.
|
||||
`extensionsdk\Microsoft.CmdPal.Extensions.Helpers.Lib\DefaultClasses.cs`.
|
||||
|
||||
In addition to the default implementations we provide for the interfaces above,
|
||||
we should provide a set of helper classes that make it easier for developers to
|
||||
@@ -1594,7 +1697,7 @@ write extensions.
|
||||
For example, we should have something like:
|
||||
|
||||
```cs
|
||||
class OpenUrlAction(string targetUrl, ActionResult result) : Microsoft.Windows.Run.Extensions.InvokableCommand {
|
||||
class OpenUrlAction(string targetUrl, CommandResult result) : Microsoft.CmdPal.Extensions.Helpers.InvokableCommand {
|
||||
public OpenUrlAction()
|
||||
{
|
||||
Name = "Open";
|
||||
@@ -1612,7 +1715,7 @@ that no longer do we need to add additional classes for the actions. We just use
|
||||
the helper:
|
||||
|
||||
```cs
|
||||
class NewsListItem : Microsoft.Windows.Run.Extensions.ListItem {
|
||||
class NewsListItem : Microsoft.CmdPal.Extensions.Helpers.ListItem {
|
||||
private NewsPost _post;
|
||||
public NewsListItem(NewsPost post)
|
||||
{
|
||||
@@ -1629,7 +1732,7 @@ class NewsListItem : Microsoft.Windows.Run.Extensions.ListItem {
|
||||
];
|
||||
public ITag[] Tags => [ new Tag(){ Text=post.Poster, new Tag(){ Text=post.Points } } ];
|
||||
}
|
||||
class HackerNewsPage: Microsoft.Windows.Run.Extensions.ListPage {
|
||||
class HackerNewsPage: Microsoft.CmdPal.Extensions.Helpers.ListPage {
|
||||
public HackerNewsPage()
|
||||
{
|
||||
Loading = true;
|
||||
@@ -1678,7 +1781,7 @@ class MyAppSettings {
|
||||
public Helpers.Settings Settings => _settings;
|
||||
|
||||
public MyAppSettings() {
|
||||
// Define the structure of your settings here.
|
||||
// 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);
|
||||
@@ -1695,11 +1798,11 @@ class MyAppSettings {
|
||||
/* 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.
|
||||
// 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
|
||||
class MySettingsPage : Microsoft.CmdPal.Extensions.Helpers.FormPage
|
||||
{
|
||||
private readonly MyAppSettings mySettings;
|
||||
public MySettingsPage(MyAppSettings s) {
|
||||
@@ -1707,26 +1810,26 @@ class MySettingsPage : Microsoft.Windows.Run.Extensions.FormPage
|
||||
mySettings.Settings.SettingsChanged += SettingsChanged;
|
||||
}
|
||||
public override IForm[] Forms() {
|
||||
// If you haven't already:
|
||||
// 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:
|
||||
// Possibly even:
|
||||
mySettings.SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// elsewhere in your app:
|
||||
|
||||
MyAppSettings instance = /* Up to you how you want to pass this around.
|
||||
Singleton, dependency injection, whatever. */
|
||||
MyAppSettings instance = /* Up to you how you want to pass this around.
|
||||
Singleton, dependency injection, whatever. */
|
||||
var onOff = instance.Settings.Get("onOff");
|
||||
|
||||
```
|
||||
@@ -1738,7 +1841,7 @@ var onOff = instance.Settings.Get("onOff");
|
||||
<img src="./grid-actions-mock.png" height="300px" /> <img src="./grid-status-loading-mock.png" height="300px" /> <img src="./grid-status-success-mock.png" height="300px" />
|
||||
|
||||
Extensions will want to be able to communicate feedback to the user, based on
|
||||
what's going on inside the extension.
|
||||
what's going on inside the extension.
|
||||
|
||||
Consider a `winget` extension that allows the user to search for and install
|
||||
packages. When the user starts an install, the extension should be able to show
|
||||
@@ -1831,20 +1934,20 @@ When displaying a command context menu item:
|
||||
* The text is `ICommandItem.Title ?? ICommandItem.Command.Name`
|
||||
* The tooltip is `ICommandItem.Subtitle`
|
||||
|
||||
When displaying a `IListItem`'s default `Command` as a context item, we'll make a new
|
||||
When displaying a `IListItem`'s default `Command` as a context item, we'll make a new
|
||||
```cs
|
||||
ICommandContextItem(){
|
||||
ICommandContextItem(){
|
||||
Command = ICommandItem.Command,
|
||||
MoreCommands = null,
|
||||
Icon = Command.Icon, // use icon from command, not list item
|
||||
Icon = Command.Icon, // use icon from command, not list item
|
||||
Title = Command.Name, // Use command's name, not list item
|
||||
Subtitle = IListItem.Title, // Use the title of the list item as the tooltip on the context menu
|
||||
IsCritical = false,
|
||||
}
|
||||
```
|
||||
|
||||
If a `ICommandItem` in a context menu has `MoreCommands`, then activating it will open a submenu with those items.
|
||||
If a `ICommandItem` in a context menu has `MoreCommands` AND a non-null `Command`, then activating it will open a submenu with the `Command` first (following the same rules above for building a context item from a default `Command`), followed by the items in `MoreCommands`.
|
||||
If a `ICommandItem` in a context menu has `MoreCommands`, then activating it will open a submenu with those items.
|
||||
If a `ICommandItem` in a context menu has `MoreCommands` AND a non-null `Command`, then activating it will open a submenu with the `Command` first (following the same rules above for building a context item from a default `Command`), followed by the items in `MoreCommands`.
|
||||
|
||||
When displaying a page:
|
||||
* The title will be `IPage.Title ?? ICommand.Name`
|
||||
@@ -1875,7 +1978,7 @@ classDiagram
|
||||
|
||||
IInvokableCommand --|> ICommand
|
||||
class IInvokableCommand {
|
||||
ICommandResult Invoke()
|
||||
ICommandResult Invoke(object context)
|
||||
}
|
||||
|
||||
class IForm {
|
||||
@@ -2028,7 +2131,7 @@ I had originally started to spec this out as:
|
||||
```cs
|
||||
interface IInvokableCommandWithParameters requires ICommand {
|
||||
ActionParameters Parameters { get; };
|
||||
ActionResult InvokeWithArgs(ActionArguments args);
|
||||
CommandResult InvokeWithArgs(ActionArguments args);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2042,7 +2145,7 @@ follow - these are not part of the current SDK spec.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> A thought: what if a action returns a `ActionResult.Entity`, then that takes
|
||||
> A thought: what if a action returns a `CommandResult.Entity`, then that takes
|
||||
> devpal back home, but leaves the entity in the query box. This would allow for
|
||||
> a Quicksilver-like "thing, do" flow. That command would prepopulate the
|
||||
> parameters. So we would then filter top-level commands based on things that can
|
||||
@@ -2050,7 +2153,7 @@ follow - these are not part of the current SDK spec.
|
||||
>
|
||||
> For example: The user uses the "Search for file" list page. They find the file
|
||||
> they're looking for. That file's ListItem has a context item "With
|
||||
> {filename}..." that then returns a `ActionResult.Entity` with the file entity.
|
||||
> {filename}..." that then returns a `CommandResult.Entity` with the file entity.
|
||||
> The user is taken back to the main page, and a file picker badge (with that
|
||||
> filename) is at the top of the search box. In that state, the only commands
|
||||
> now shown are ones that can accept a File entity. This could be things like
|
||||
@@ -2101,6 +2204,8 @@ Is that just a `Details` object? A markdown body?
|
||||
|
||||
## Footnotes
|
||||
|
||||
### Generating the `.idl`
|
||||
|
||||
The `.idl` for this SDK can be generated directly from this file. To do so, run the following command:
|
||||
|
||||
```ps1
|
||||
@@ -2112,9 +2217,16 @@ The `.idl` for this SDK can be generated directly from this file. To do so, run
|
||||
Or, to generate straight to the place I'm consuming it from:
|
||||
|
||||
```ps1
|
||||
.\doc\initial-sdk-spec\generate-interface.ps1 > .\extensionsdk\Microsoft.CmdPal.Extensions\Microsoft.Windows.Run.Extensions.idl
|
||||
.\doc\initial-sdk-spec\generate-interface.ps1 > .\extensionsdk\Microsoft.CmdPal.Extensions\Microsoft.CmdPal.Extensions.Helpers.idl
|
||||
```
|
||||
|
||||
### Adding APIs
|
||||
|
||||
Almost all of the SDK defined here is in terms of interfaces. Unfortunately,
|
||||
this prevents us from being able to use `[contract]` attributes to add to the
|
||||
interfaces. We'll instead need to rely on the tried-and-true method of adding a
|
||||
`IFoo2` when we want to add methods to `IFoo`.
|
||||
|
||||
[^1]: In this example, as in other places, I've referenced a
|
||||
`Microsoft.DevPal.Extensions.InvokableCommand` class, as the base for that action.
|
||||
Our SDK will include partial class implementations for interfaces like
|
||||
@@ -2141,4 +2253,4 @@ Or, to generate straight to the place I'm consuming it from:
|
||||
palette re-cache the results of `TopLevelItems`.
|
||||
|
||||
[Dev Home Extension]: https://learn.microsoft.com/en-us/windows/dev-home/extensions
|
||||
[`ISupportIncrementalLoading`]: https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.data.isupportincrementalloading
|
||||
[`ISupportIncrementalLoading`]: https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.data.isupportincrementalloading
|
||||
|
||||
@@ -6,20 +6,15 @@ namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public class CommandResult : ICommandResult
|
||||
{
|
||||
// TODO: is Args needed?
|
||||
private ICommandResultArgs? _args;
|
||||
private CommandResultKind _kind = CommandResultKind.Dismiss;
|
||||
public ICommandResultArgs? Args { get; private set; }
|
||||
|
||||
// TODO: is Args needed?
|
||||
public ICommandResultArgs? Args => _args;
|
||||
|
||||
public CommandResultKind Kind => _kind;
|
||||
public CommandResultKind Kind { get; private set; } = CommandResultKind.Dismiss;
|
||||
|
||||
public static CommandResult Dismiss()
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
_kind = CommandResultKind.Dismiss,
|
||||
Kind = CommandResultKind.Dismiss,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,8 +22,8 @@ public class CommandResult : ICommandResult
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
_kind = CommandResultKind.GoHome,
|
||||
_args = null,
|
||||
Kind = CommandResultKind.GoHome,
|
||||
Args = null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,8 +31,8 @@ public class CommandResult : ICommandResult
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
_kind = CommandResultKind.GoBack,
|
||||
_args = null,
|
||||
Kind = CommandResultKind.GoBack,
|
||||
Args = null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,8 +40,8 @@ public class CommandResult : ICommandResult
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
_kind = CommandResultKind.Hide,
|
||||
_args = null,
|
||||
Kind = CommandResultKind.Hide,
|
||||
Args = null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,8 +49,44 @@ public class CommandResult : ICommandResult
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
_kind = CommandResultKind.KeepOpen,
|
||||
_args = null,
|
||||
Kind = CommandResultKind.KeepOpen,
|
||||
Args = null,
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult GoToPage(GoToPageArgs args)
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.GoToPage,
|
||||
Args = args,
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult ShowToast(ToastArgs args)
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.ShowToast,
|
||||
Args = args,
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult ShowToast(string message)
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.ShowToast,
|
||||
Args = new ToastArgs() { Message = message },
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult Confirm(ConfirmationArgs args)
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.Confirm,
|
||||
Args = args,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public partial class ConfirmationArgs : IConfirmationArgs
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
|
||||
public string? Description { get; set; }
|
||||
|
||||
public ICommand? PrimaryCommand { get; set; }
|
||||
|
||||
public bool IsPrimaryCommandCritical { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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 Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public abstract partial class ContentPage : Page, IContentPage
|
||||
{
|
||||
public event TypedEventHandler<object, ItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public IDetails? Details
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Details));
|
||||
}
|
||||
}
|
||||
|
||||
public IContextItem[] Commands { get; set; } = [];
|
||||
|
||||
public abstract IContent[] GetContent();
|
||||
|
||||
protected void RaiseItemsChanged(int totalItems)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO #181 - This is the same thing that BaseObservable has to deal with.
|
||||
ItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public partial class DetailsCommand : IDetailsCommand
|
||||
{
|
||||
public ICommand? Command { get; set; }
|
||||
}
|
||||
@@ -9,4 +9,23 @@ public partial class DetailsLink : IDetailsLink
|
||||
public Uri? Link { get; set; }
|
||||
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
public DetailsLink()
|
||||
{
|
||||
}
|
||||
|
||||
public DetailsLink(string url)
|
||||
: this(url, url)
|
||||
{
|
||||
}
|
||||
|
||||
public DetailsLink(string url, string text)
|
||||
{
|
||||
if (Uri.TryCreate(url, default(UriCreationOptions), out var newUri))
|
||||
{
|
||||
Link = newUri;
|
||||
}
|
||||
|
||||
Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public abstract class Form : IForm
|
||||
public abstract partial class Form : IForm
|
||||
{
|
||||
public virtual string Data { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public partial class FormContent : BaseObservable, IFormContent
|
||||
{
|
||||
public virtual string DataJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(DataJson));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string StateJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(StateJson));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string TemplateJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(TemplateJson));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual ICommandResult SubmitForm(string payload) => CommandResult.KeepOpen();
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public class GoToPageArgs : IGoToPageArgs
|
||||
public partial class GoToPageArgs : IGoToPageArgs
|
||||
{
|
||||
public required string PageId { get; set; }
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public class InvokableCommand : Command, IInvokableCommand
|
||||
public abstract class InvokableCommand : Command, IInvokableCommand
|
||||
{
|
||||
public virtual ICommandResult Invoke() => throw new NotImplementedException();
|
||||
public virtual ICommandResult Invoke() => CommandResult.KeepOpen();
|
||||
|
||||
public virtual ICommandResult Invoke(object? sender) => Invoke();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public partial class MarkdownContent : BaseObservable, IMarkdownContent
|
||||
{
|
||||
public virtual string Body
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Body));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>Microsoft.CmdPal.Extensions.Helpers.pri</ProjectPriFileName>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
|
||||
@@ -6,38 +6,36 @@ namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public partial class StatusMessage : BaseObservable, IStatusMessage
|
||||
{
|
||||
private MessageState _messageState = MessageState.Info;
|
||||
|
||||
private string _message = string.Empty;
|
||||
|
||||
private IProgressState? _progressState;
|
||||
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
get;
|
||||
set
|
||||
{
|
||||
_message = value;
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Message));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public MessageState State
|
||||
{
|
||||
get => _messageState;
|
||||
get;
|
||||
set
|
||||
{
|
||||
_messageState = value;
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(State));
|
||||
}
|
||||
}
|
||||
|
||||
= MessageState.Info;
|
||||
|
||||
public IProgressState? Progress
|
||||
{
|
||||
get => _progressState;
|
||||
get;
|
||||
set
|
||||
{
|
||||
_progressState = value;
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Progress));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ public class Tag : BaseObservable, ITag
|
||||
{
|
||||
private OptionalColor _foreground;
|
||||
private OptionalColor _background;
|
||||
private IconInfo _icon = new(string.Empty);
|
||||
private string _text = string.Empty;
|
||||
private string _toolTip = string.Empty;
|
||||
private ICommand? _command;
|
||||
|
||||
public OptionalColor Foreground
|
||||
{
|
||||
@@ -35,14 +32,16 @@ public class Tag : BaseObservable, ITag
|
||||
|
||||
public IconInfo Icon
|
||||
{
|
||||
get => _icon;
|
||||
get;
|
||||
set
|
||||
{
|
||||
_icon = value;
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
= new(string.Empty);
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => _text;
|
||||
@@ -55,23 +54,15 @@ public class Tag : BaseObservable, ITag
|
||||
|
||||
public string ToolTip
|
||||
{
|
||||
get => _toolTip;
|
||||
get;
|
||||
set
|
||||
{
|
||||
_toolTip = value;
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ToolTip));
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand? Command
|
||||
{
|
||||
get => _command;
|
||||
set
|
||||
{
|
||||
_command = value;
|
||||
OnPropertyChanged(nameof(Command));
|
||||
}
|
||||
}
|
||||
= string.Empty;
|
||||
|
||||
public Tag()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public partial class ToastArgs : IToastArgs
|
||||
{
|
||||
public string? Message { get; set; }
|
||||
|
||||
public ICommandResult? Result { get; set; } = CommandResult.Dismiss();
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public partial class ToastStatusMessage
|
||||
{
|
||||
private readonly Lock _showLock = new();
|
||||
private bool _shown;
|
||||
|
||||
public StatusMessage Message { get; init; }
|
||||
|
||||
public int Duration { get; init; } = 2500;
|
||||
|
||||
public ToastStatusMessage(StatusMessage message)
|
||||
{
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public ToastStatusMessage(string text)
|
||||
{
|
||||
Message = new StatusMessage() { Message = text };
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
lock (_showLock)
|
||||
{
|
||||
if (!_shown)
|
||||
{
|
||||
ExtensionHost.ShowStatus(Message);
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
Thread.Sleep(Duration);
|
||||
|
||||
lock (_showLock)
|
||||
{
|
||||
_shown = false;
|
||||
ExtensionHost.HideStatus(Message);
|
||||
}
|
||||
});
|
||||
_shown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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 Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
public partial class TreeContent : BaseObservable, ITreeContent
|
||||
{
|
||||
public IContent[] Children { get; set; } = [];
|
||||
|
||||
public virtual IContent? RootContent
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(RootContent));
|
||||
}
|
||||
}
|
||||
|
||||
public event TypedEventHandler<object, ItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public virtual IContent[] GetChildren() => Children;
|
||||
|
||||
protected void RaiseItemsChanged(int totalItems)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO #181 - This is the same thing that BaseObservable has to deal with.
|
||||
ItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace Microsoft.CmdPal.Extensions
|
||||
String Icon { get; };
|
||||
Windows.Storage.Streams.IRandomAccessStreamReference Data { get; };
|
||||
};
|
||||
|
||||
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
runtimeclass IconInfo {
|
||||
IconInfo(String iconString);
|
||||
@@ -78,9 +78,11 @@ namespace Microsoft.CmdPal.Extensions
|
||||
Dismiss, // Reset the palette to the main page and dismiss
|
||||
GoHome, // Go back to the main page, but keep it open
|
||||
GoBack, // Go back one level
|
||||
Hide, // Keep this page open, but hide the palette.
|
||||
Hide, // Keep this page open, but hide the palette.
|
||||
KeepOpen, // Do nothing.
|
||||
GoToPage, // Go to another page. GoToPageArgs will tell you where.
|
||||
ShowToast, // Display a transient message to the user
|
||||
Confirm, // Display a confirmation dialog
|
||||
};
|
||||
|
||||
enum NavigationMode {
|
||||
@@ -102,15 +104,28 @@ namespace Microsoft.CmdPal.Extensions
|
||||
String PageId { get; };
|
||||
NavigationMode NavigationMode { get; };
|
||||
}
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IToastArgs requires ICommandResultArgs{
|
||||
String Message { get; };
|
||||
ICommandResult Result { get; };
|
||||
}
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IConfirmationArgs requires ICommandResultArgs{
|
||||
String Title { get; };
|
||||
String Description { get; };
|
||||
ICommand PrimaryCommand { get; };
|
||||
Boolean IsPrimaryCommandCritical { get; };
|
||||
}
|
||||
|
||||
// This is a "leaf" of the UI. This is something that can be "done" by the user.
|
||||
// * A ListPage
|
||||
// * the MoreCommands flyout of for a ListItem or a MarkdownPage
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IInvokableCommand requires ICommand {
|
||||
ICommandResult Invoke();
|
||||
ICommandResult Invoke(Object sender);
|
||||
}
|
||||
|
||||
|
||||
[uuid("ef5db50c-d26b-4aee-9343-9f98739ab411")]
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IFilterItem {}
|
||||
@@ -153,7 +168,6 @@ namespace Microsoft.CmdPal.Extensions
|
||||
OptionalColor Foreground { get; };
|
||||
OptionalColor Background { get; };
|
||||
String ToolTip { get; };
|
||||
ICommand Command { get; };
|
||||
};
|
||||
|
||||
[uuid("6a6dd345-37a3-4a1e-914d-4f658a4d583d")]
|
||||
@@ -180,6 +194,10 @@ namespace Microsoft.CmdPal.Extensions
|
||||
Windows.Foundation.Uri Link { get; };
|
||||
String Text { get; };
|
||||
}
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IDetailsCommand requires IDetailsData {
|
||||
ICommand Command { get; };
|
||||
}
|
||||
[uuid("58070392-02bb-4e89-9beb-47ceb8c3d741")]
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IDetailsSeparator requires IDetailsData {}
|
||||
@@ -231,7 +249,7 @@ namespace Microsoft.CmdPal.Extensions
|
||||
interface IPage requires ICommand {
|
||||
String Title { get; };
|
||||
Boolean IsLoading { get; };
|
||||
|
||||
|
||||
OptionalColor AccentColor { get; };
|
||||
}
|
||||
|
||||
@@ -246,7 +264,7 @@ namespace Microsoft.CmdPal.Extensions
|
||||
IconInfo Icon{ get; };
|
||||
String Title{ get; };
|
||||
String Subtitle{ get; };
|
||||
}
|
||||
}
|
||||
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface ICommandContextItem requires ICommandItem, IContextItem {
|
||||
@@ -273,7 +291,7 @@ namespace Microsoft.CmdPal.Extensions
|
||||
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IListPage requires IPage, INotifyItemsChanged {
|
||||
// DevPal will be responsible for filtering the list of items, unless the
|
||||
// DevPal will be responsible for filtering the list of items, unless the
|
||||
// class implements IDynamicListPage
|
||||
String SearchText { get; };
|
||||
String PlaceholderText { get; };
|
||||
@@ -283,7 +301,7 @@ namespace Microsoft.CmdPal.Extensions
|
||||
Boolean HasMoreItems { get; };
|
||||
ICommandItem EmptyContent { get; };
|
||||
|
||||
IListItem[] GetItems();
|
||||
IListItem[] GetItems();
|
||||
void LoadMore();
|
||||
}
|
||||
|
||||
@@ -312,9 +330,40 @@ namespace Microsoft.CmdPal.Extensions
|
||||
IForm[] Forms();
|
||||
}
|
||||
|
||||
[uuid("b64def0f-8911-4afa-8f8f-042bd778d088")]
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IContent requires INotifyPropChanged {
|
||||
}
|
||||
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IFormContent requires IContent {
|
||||
String TemplateJson { get; };
|
||||
String DataJson { get; };
|
||||
String StateJson { get; };
|
||||
ICommandResult SubmitForm(String payload);
|
||||
}
|
||||
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IMarkdownContent requires IContent {
|
||||
String Body { get; };
|
||||
}
|
||||
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface ITreeContent requires IContent, INotifyItemsChanged {
|
||||
IContent RootContent { get; };
|
||||
IContent[] GetChildren();
|
||||
}
|
||||
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface IContentPage requires IPage, INotifyItemsChanged {
|
||||
IContent[] GetContent();
|
||||
IDetails Details { get; };
|
||||
IContextItem[] Commands { get; };
|
||||
}
|
||||
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
interface ICommandSettings {
|
||||
IFormPage SettingsPage { get; };
|
||||
IContentPage SettingsPage { get; };
|
||||
};
|
||||
|
||||
[contract(Microsoft.CmdPal.Extensions.ExtensionsContract, 1)]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "This is sample code")]
|
||||
@@ -0,0 +1,181 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
internal sealed partial class SampleCommentsPage : ContentPage
|
||||
{
|
||||
private readonly TreeContent myContentTree;
|
||||
|
||||
public override IContent[] GetContent() => [myContentTree];
|
||||
|
||||
public SampleCommentsPage()
|
||||
{
|
||||
Name = "View Posts";
|
||||
Icon = new("\uE90A"); // Comment
|
||||
|
||||
myContentTree = new()
|
||||
{
|
||||
RootContent = new MarkdownContent()
|
||||
{
|
||||
Body = """
|
||||
# Example of a thread of comments
|
||||
You can use TreeContent in combination with FormContent to build a structure like a page with comments.
|
||||
|
||||
The forms on this page use the AdaptiveCard `Action.ShowCard` action to show a nested, hidden card on the form.
|
||||
""",
|
||||
},
|
||||
|
||||
Children = [
|
||||
new PostContent("First")
|
||||
{
|
||||
Replies = [
|
||||
new PostContent("Oh very insightful. I hadn't considered that"),
|
||||
new PostContent("Second"),
|
||||
new PostContent("ah the ol switcheroo"),
|
||||
],
|
||||
},
|
||||
new PostContent("First\nEDIT: shoot")
|
||||
{
|
||||
Replies = [
|
||||
new PostContent("delete this"),
|
||||
],
|
||||
},
|
||||
new PostContent("Do you think they get the picture")
|
||||
{
|
||||
Replies = [
|
||||
new PostContent("Probably! Now go build and be happy"),
|
||||
],
|
||||
}
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
|
||||
internal sealed partial class PostContent : TreeContent
|
||||
{
|
||||
public List<IContent> Replies { get; init; } = [];
|
||||
|
||||
private readonly ToastStatusMessage _toast = new(new StatusMessage() { Message = "Reply posted", State = MessageState.Success });
|
||||
|
||||
public PostContent(string body)
|
||||
{
|
||||
RootContent = new PostForm(body, this);
|
||||
}
|
||||
|
||||
public override IContent[] GetChildren() => Replies.ToArray();
|
||||
|
||||
public void Post()
|
||||
{
|
||||
RaiseItemsChanged(Replies.Count);
|
||||
_toast.Show();
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
|
||||
internal sealed partial class PostForm : FormContent
|
||||
{
|
||||
private readonly PostContent _parent;
|
||||
|
||||
public PostForm(string postBody, PostContent parent)
|
||||
{
|
||||
_parent = parent;
|
||||
TemplateJson = """
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${postBody}",
|
||||
"wrap": true
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.ShowCard",
|
||||
"title": "${replyCard.title}",
|
||||
"card": {
|
||||
"type": "AdaptiveCard",
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "Container",
|
||||
"id": "${replyCard.idPrefix}Properties",
|
||||
"items": [
|
||||
{
|
||||
"$data": "${replyCard.fields}",
|
||||
"type": "Input.Text",
|
||||
"label": "${label}",
|
||||
"id": "${id}",
|
||||
"isRequired": "${required}",
|
||||
"isMultiline": true,
|
||||
"errorMessage": "'${label}' is required"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Post"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Favorite"
|
||||
},
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "View on web"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
DataJson = $$"""
|
||||
{
|
||||
"postBody": {{JsonSerializer.Serialize(postBody)}},
|
||||
"replyCard": {
|
||||
"title": "Reply",
|
||||
"idPrefix": "reply",
|
||||
"fields": [
|
||||
{
|
||||
"label": "Reply",
|
||||
"id": "ReplyBody",
|
||||
"required": true,
|
||||
"placeholder": "Write a reply here"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
public override ICommandResult SubmitForm(string payload)
|
||||
{
|
||||
var data = JsonNode.Parse(payload);
|
||||
_ = data;
|
||||
var reply = data["ReplyBody"];
|
||||
var s = reply?.AsValue()?.ToString();
|
||||
if (!string.IsNullOrEmpty(s))
|
||||
{
|
||||
_parent.Replies.Add(new PostContent(s));
|
||||
_parent.Post();
|
||||
}
|
||||
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user