mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
a lot of notes here. Maybe we should write real code now
This commit is contained in:
@@ -0,0 +1,517 @@
|
||||
---
|
||||
author: Mike Griese
|
||||
created on: 2025-09-04
|
||||
last updated: 2025-09-08
|
||||
issue id: n/a
|
||||
---
|
||||
|
||||
## Addenda II-D: Parameters page
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```c#
|
||||
interface IParameterRun requires INotifyPropChanged
|
||||
{
|
||||
|
||||
};
|
||||
|
||||
interface ILabelRun requires IParameterRun
|
||||
{
|
||||
String Text { get; };
|
||||
};
|
||||
|
||||
interface IParameterRun requires IParameterRun
|
||||
{
|
||||
String PlaceholderText { get; };
|
||||
Boolean NeedsValue { get; } // TODO! name is weird
|
||||
};
|
||||
|
||||
interface IStringParameterRun requires IParameterRun
|
||||
{
|
||||
String Text { get; set; };
|
||||
|
||||
// TODO! do we need a way to validate string inputs?
|
||||
};
|
||||
|
||||
interface ICommandParameterRun requires IParameterRun
|
||||
{
|
||||
String DisplayText { get; };
|
||||
ICommand GetSelectValueCommand(UInt64 hostHwnd);
|
||||
IIconInfo Icon { get; }; // ? maybe
|
||||
|
||||
};
|
||||
|
||||
interface IParametersPage requires IPage
|
||||
{
|
||||
IParameterRun[] Parameters { get; };
|
||||
IListItem Command { get; };
|
||||
};
|
||||
```
|
||||
|
||||
When we open a `IParametersPage`, we will render the `Parameters` in the search
|
||||
box. We'll move focus to the first `IParameterRun` that is not a `ILabelRun`.
|
||||
What those interactions looks like depends on the type of `IParameterRun`.
|
||||
|
||||
There are three basic types of inputs: strings, invokable commands, and lists.
|
||||
Strings are a special case that doesn't require a command to set the value.
|
||||
Lists and invokable commands are picked based on the type of the
|
||||
`SelectValueCommand`. Each of these are detailed below.
|
||||
|
||||
When all the parameters have `NeedsValue` set to `false`, we will display a
|
||||
single item to the user - the `Command` item.
|
||||
|
||||
### String parameters
|
||||
|
||||
These are rendered as a text box within the search box. The user can type into
|
||||
it. Focus is moved to the next parameter when the user presses Enter or tab.
|
||||
|
||||
### Command parameters - Invokable Commands
|
||||
|
||||
These are used when the `SelectValueCommand` is an `IInvokableCommand`.
|
||||
|
||||
These are rendered as a button within the search box. The button text is
|
||||
`DisplayText` if it is set, otherwise it is `PlaceholderText`. If the user
|
||||
clicks the button, we invoke the `SelectValueCommand` (and ignore the `CommandResult`).
|
||||
|
||||
This is good for file pickers, date pickers, color pickers, etc. Anything that
|
||||
requires a custom UI to pick a value.
|
||||
|
||||
When the extension has picked a value, it should set the `NeedsValue` to false.
|
||||
The extension can also set the `DisplayText` and `Icon` to reflect the chosen value.
|
||||
|
||||
When the user presses enter with the button focused, we will also invoke the
|
||||
`SelectValueCommand`.
|
||||
|
||||
When the user presses tab, we will move focus to the next parameter.
|
||||
|
||||
If the `NeedsValue` property is changed to `false` while it's focused, we will
|
||||
move focus to the next parameter.
|
||||
|
||||
### Command parameters - List Commands
|
||||
|
||||
These are used when the `SelectValueCommand` is an `IListPage` - both static and
|
||||
dynamic lists work similarly.
|
||||
|
||||
These are rendered as a text box within the search box. When the user focuses
|
||||
the text box, we will display the items from the `IListPage` in the body of
|
||||
CmdPal. The user can then type to filter the list. This filtering will work the
|
||||
same way as any other list page in CmdPal - CmdPal will filter static lists, or
|
||||
pass the query to a dynamic list.
|
||||
|
||||
The items in this list should all be `IListItem` objects with
|
||||
`IInvokableCommands`. Putting a `IPage` into one of these items will cause the
|
||||
user to navigate away from the parameters page, which would probably be
|
||||
unexpected.
|
||||
|
||||
When the user picks an item from the list, the extension should handle that
|
||||
command by bubbling an event up to the `CommandRun`, and setting the `Value`,
|
||||
`DisplayText`, and `Icon` properties, and setting `NeedsValue` to false.
|
||||
|
||||
When the user presses enter with the text box focused, we will invoke the
|
||||
command of the selected item in the list.
|
||||
|
||||
When the user presses tab, we will move focus to the next parameter.
|
||||
|
||||
If the `NeedsValue` property is changed to `false` while it's focused, we will
|
||||
move focus to the next parameter.
|
||||
|
||||
### Example
|
||||
|
||||
Lets say you had a command like "Create a note \${title} in \${folder}".
|
||||
`title` is a string input, and `folder` is a static list of folders.
|
||||
|
||||
The extension author can then define a `IParametersPage` with four runs in it:
|
||||
* A `ILabelRun` for "Create a note"
|
||||
* A `IStringParameterRun` for the `title`
|
||||
* A `ILabelRun` for "in"
|
||||
* A `ICommandParameterRun` for the `folder`. The `Command` will be a `IListPage`, where the items are possible folders
|
||||
|
||||
|
||||
In this example, the user can pick the "create note" command, then type the title, hit enter/tab, and then pick a folder from the list, then hit enter to run the command.
|
||||
|
||||
```cs
|
||||
public interface IRequiresHostHwnd
|
||||
{
|
||||
void SetHostHwnd(UInt64 hostHwnd);
|
||||
}
|
||||
|
||||
public sealed partial class CommandParameterRun : BaseObservable, ICommandParameterRun
|
||||
{
|
||||
public virtual string DisplayText { get; set; } // basic projected properties here, same as throughout the toolkit
|
||||
public virtual string PlaceholderText { get; set; } // basic projected properties here, same as throughout the toolkit
|
||||
public virtual ICommand Command { get; set; } // basic projected properties here, same as throughout the toolkit
|
||||
public virtual IIconInfo Icon { get; set; } // basic projected properties here, same as throughout the toolkit
|
||||
public virtual bool NeedsValue => Value == null; // Toolkit helper: does this parameter need a value?
|
||||
|
||||
public virtual ICommand GetSelectValueCommand(UInt64 hostHwnd)
|
||||
{
|
||||
if (Command is IRequiresHostHwnd requiresHwnd)
|
||||
{
|
||||
requiresHwnd.SetHostHwnd(hostHwnd);
|
||||
}
|
||||
return Command;
|
||||
}
|
||||
|
||||
public object? Value { get; set; } // Toolkit helper: a value for the parameter
|
||||
}
|
||||
|
||||
public sealed partial class CreateNoteParametersPage : ParametersPage
|
||||
{
|
||||
private readonly SelectFolderPage _selectFolderPage = new SelectFolderPage();
|
||||
|
||||
private readonly StringParameterRun _titleParameter = new StringParameterRun()
|
||||
{
|
||||
PlaceholderText = "Note title"
|
||||
};
|
||||
private readonly ICommandParameterRun _folderParameter = new CommandParameterRun()
|
||||
{
|
||||
PlaceholderText = "Select folder",
|
||||
Command = _selectFolderPage
|
||||
};
|
||||
|
||||
private readonly List<IParameterRun> _parameters;
|
||||
|
||||
private readonly CreateNoteCommand _command = new() { TitleParameter = _titleParameter, FolderParameter = _folderParameter };
|
||||
private readonly ListItem _item = new(_command);
|
||||
|
||||
public IParameterRun[] Parameters => _parameters.ToArray();
|
||||
public IListItem Command => _item;
|
||||
|
||||
public CreateNoteParametersPage()
|
||||
{
|
||||
_parameters = new List<IParameterRun>
|
||||
{
|
||||
new LabelRun("Create a note"),
|
||||
_titleParameter,
|
||||
new LabelRun("in"),
|
||||
_folderParameter
|
||||
};
|
||||
|
||||
_selectFolderPage.FolderSelected += (s, folder) =>
|
||||
{
|
||||
_folderParameter.Value = folder;
|
||||
_folderParameter.Icon = folder.Icon;
|
||||
_folderParameter.DisplayText = folder.Name;
|
||||
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
public sealed partial class CreateNoteCommand : BaseObservable, IInvokableCommand
|
||||
{
|
||||
internal IStringParameterRun TitleParameter { get; init; } // set by the parameters page
|
||||
internal ICommandParameterRun FolderParameter { get; init; } // set by the parameters page
|
||||
|
||||
public IIconInfo Icon => new IconInfo("NoteAdd");
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
var title = TitleParameter.Text;
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
var t = new ToastStatusMessage(new StatusMessage(){ Title = "Title is required", State = MessageState.Error });
|
||||
t.Show();
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
var folder = FolderParameter.Value;
|
||||
if (folder is not Folder)
|
||||
{
|
||||
// This is okay, we'll create the note in the default folder
|
||||
}
|
||||
|
||||
// Create the note in the specified folder
|
||||
NoteService.CreateNoteInFolder(title, folder); // whatever your backend is
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class SelectFolderPage : ListPage
|
||||
{
|
||||
public event EventHandler<Folder>? FolderSelected;
|
||||
|
||||
public SelectFolderPage()
|
||||
{
|
||||
// Populate the list with folders
|
||||
var folders = FolderService.GetFolders(); // whatever your backend is
|
||||
Items = folders.Select(f => new ListItem(new SelectFolderCommand(f), f.Name, f.Icon)).ToArray();
|
||||
}
|
||||
|
||||
private sealed partial class SelectFolderCommand : BaseObservable, IInvokableCommand
|
||||
{
|
||||
private readonly EventHandler<Folder> _folderSelected;
|
||||
private readonly Folder _folder;
|
||||
|
||||
public IIconInfo Icon => _folder.Icon;
|
||||
public string Title => _folder.Name;
|
||||
|
||||
public SelectFolderCommand(Folder folder, EventHandler<Folder> folderSelected)
|
||||
{
|
||||
_folder = folder;
|
||||
_folderSelected = folderSelected;
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
_folderSelected?.Invoke(this, _folder);
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class FilePickerParameterRun : CommandParameterRun
|
||||
{
|
||||
public StorageFile? File { get; private set;}
|
||||
public FilePickerParameterRun()
|
||||
{
|
||||
var command = new FilePickerCommand();
|
||||
command.FileSelected += (file) =>
|
||||
{
|
||||
File = file;
|
||||
if (file != null)
|
||||
{
|
||||
Value = file;
|
||||
DisplayText = file.Name;
|
||||
// Icon = new IconInfo("File");
|
||||
}
|
||||
else
|
||||
{
|
||||
Value = null;
|
||||
DisplayText = null;
|
||||
// Icon = new IconInfo("File");
|
||||
}
|
||||
};
|
||||
PlaceholderText = "Select a file";
|
||||
Icon = new IconInfo("File");
|
||||
Command = command;
|
||||
}
|
||||
|
||||
private sealed partial class FilePickerCommand : InvokableCommand, IRequiresHostHwnd
|
||||
{
|
||||
public IIconInfo Icon => new IconInfo("File");
|
||||
public string Name => "Pick a file";
|
||||
|
||||
public event EventHandler<StorageFile?>? FileSelected;
|
||||
|
||||
private uint _hostHwnd;
|
||||
|
||||
public void SetHostHwnd(uint hostHwnd)
|
||||
{
|
||||
_hostHwnd = hostHwnd;
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
PickFileAsync();
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
private async void PickFileAsync()
|
||||
{
|
||||
var picker = new Windows.Storage.Pickers.FileOpenPicker();
|
||||
// You need to initialize the picker with a window handle in WinUI 3 desktop apps
|
||||
// See https://learn.microsoft.com/en-us/windows/apps/design/controls/file-open-picker
|
||||
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
|
||||
WinRT.Interop.InitializeWithWindow.Initialize(picker, _hostHwnd);
|
||||
|
||||
var file = await picker.PickSingleFileAsync();
|
||||
FileSelected?.Invoke(this, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class SelectParameterCommand<T> : InvokableCommand
|
||||
{
|
||||
public event TypedEventHandler<object, T>? ValueSelected;
|
||||
private T _value;
|
||||
public T Value { get => _value; protected set { _value = value; } }
|
||||
public SelectParameterCommand(T value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
ValueSelected?.Invoke(this, _value);
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
public sealed partial class StaticParameterList<T> : ListPage
|
||||
{
|
||||
public event TypedEventHandler<object, T>? ValueSelected;
|
||||
private bool _isInitialized = false;
|
||||
private readonly IEnumerable<T> _values;
|
||||
private readonly List<IListItem> _items = new List<IListItem>();
|
||||
private Func<T, ListItem, ListItem> _customizeListItemsCallback;
|
||||
|
||||
// ctor takes an IEnumerable<T> values, and a function to customize the ListItem's depending on the value
|
||||
public StaticParameterList(IEnumerable<T> values, Func<T, ListItem> customizeListItem)
|
||||
{
|
||||
_values = values;
|
||||
_customizeListItemsCallback = (value, listItem) => { customizeListItem(value); return listItem; };
|
||||
}
|
||||
}
|
||||
public StaticParameterList(IEnumerable<T> values, Func<T, ListItem, ListItem> customizeListItem)
|
||||
{
|
||||
_values = values;
|
||||
_customizeListItemsCallback = customizeListItem;
|
||||
}
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
Initialize(_values, _customizeListItemsCallback);
|
||||
_isInitialized = true;
|
||||
}
|
||||
return _items.ToArray();
|
||||
}
|
||||
private void Initialize(IEnumerable<T> values, Func<T, ListItem, ListItem> customizeListItem)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var command = new SelectParameterCommand<T>(value);
|
||||
command.ValueSelected += (s, v) => ValueSelected?.Invoke(this, v);
|
||||
var listItem = new ListItem(command);
|
||||
var item = customizeListItem(value, listItem);
|
||||
_items.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
--------------------------------------------------------
|
||||
|
||||
## original draft starts here
|
||||
|
||||
|
||||
### Arbitrary parameters and arguments
|
||||
|
||||
Something we'll want to consider soon is how to allow for arbitrary parameters
|
||||
to be passed to commands. This allows for commands to require additional info from
|
||||
the user _before_ they are run. In its simplest form, this is a lightweight way
|
||||
to have an action accept form data inline with the query. But this also allows
|
||||
for highly complex action chaining.
|
||||
|
||||
I had originally started to spec this out as:
|
||||
|
||||
```cs
|
||||
enum ParameterType
|
||||
{
|
||||
Text,
|
||||
File,
|
||||
Files,
|
||||
Enum,
|
||||
Entity
|
||||
};
|
||||
|
||||
interface ICommandParameter
|
||||
{
|
||||
ParameterType Type { get; };
|
||||
String Name { get; };
|
||||
Boolean Required{ get; };
|
||||
// TODO! values for enums?
|
||||
// TODO! dynamic values for enums? like GetValues(string query)
|
||||
// TODO! files might want to restrict types? but now we're a file picker and need that whole API
|
||||
// TODO! parameters with more than one value? Like,
|
||||
// SendMessage(People[] to, String message)
|
||||
};
|
||||
|
||||
interface ICommandArgument
|
||||
{
|
||||
String Name { get; };
|
||||
Object Value { get; };
|
||||
};
|
||||
|
||||
interface IInvokableCommandWithParameters requires ICommand {
|
||||
ICommandParameter[] Parameters { get; };
|
||||
ICommandResult InvokeWithArgs(Object sender, ICommandArgument[] args);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
TODO! Mike:
|
||||
We should add like, a `CustomPicker` parameter type, which would allow
|
||||
extensions to define their own custom pickers for parameters. Then when we go to fill the argument, we'd call something like `ShowPickerAsync(ICommandParameter param)` and let them fill in the value. We don't care what the value is.
|
||||
|
||||
So it'd be more like
|
||||
|
||||
```c#
|
||||
enum ParameterType
|
||||
{
|
||||
Text,
|
||||
// File,
|
||||
// Files,
|
||||
Enum,
|
||||
Custom
|
||||
};
|
||||
|
||||
// interface IArgumentEnumValue requires INotifyPropChanged
|
||||
// {
|
||||
// String Name { get; };
|
||||
// IIconInfo Icon { get; };
|
||||
// }
|
||||
interface ICommandArgument requires INotifyPropChanged
|
||||
{
|
||||
ParameterType Type { get; };
|
||||
String Name { get; };
|
||||
Boolean Required{ get; };
|
||||
|
||||
Object Value { get; set; };
|
||||
String DisplayName { get; };
|
||||
IIconInfo Icon { get; };
|
||||
|
||||
void ShowPicker(UInt64 hostHwnd);
|
||||
// todo
|
||||
// IArgumentEnumValue[] GetValues();
|
||||
};
|
||||
|
||||
interface IInvokableCommandWithParameters requires ICommand {
|
||||
ICommandArgument[] Parameters { get; };
|
||||
ICommandResult InvokeWithArgs(Object sender, ICommandArgument[] args);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
And `CommandParameters` would be a set of `{ type, name, required }` structs,
|
||||
which would specify the parameters that the action needs. Simple types would be
|
||||
`string`, `file`, `file[]`, `enum` (with possible values), etc.
|
||||
|
||||
But that may not be complex enough. We recently learned about Action Framework
|
||||
and some of their plans there - that may be a good fit for this. My raw notes
|
||||
follow - these are not part of the current SDK spec.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 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
|
||||
> accept the entity in the search box.
|
||||
>
|
||||
> 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 `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
|
||||
> the "Remove background" action (from REDACTED), the "Open with" action, the
|
||||
> "Send to Teams chat" (which would then ask for another entity). If they did
|
||||
> the "Remove Background" one, that could then return _another_ entity.
|
||||
>
|
||||
> We'd need to solve for the REDACTED case specifically, cause I bet they want to
|
||||
> stay in the REDACTED action page, rather than the main one.
|
||||
>
|
||||
> We'd also probably want the REDACTED one to be able to accept arbitrary
|
||||
> entities... like, they probably want a `+` button that lets you add... any
|
||||
> kind of entity to their page, rather than explicitly ask for a list of args.
|
||||
|
||||
However, we do not have enough visibility on how action framework actually
|
||||
works, consumer-wise, to be able to specify more. As absolutely fun as chaining
|
||||
actions together sounds, I've decided to leave this out of the official v1 spec.
|
||||
We can ship a viable v0.1 of DevPal without it, and add it in post.
|
||||
@@ -432,3 +432,96 @@ sequenceDiagram
|
||||
%% H->>BST: Focus previous BasicStringToken
|
||||
%% end
|
||||
```
|
||||
|
||||
### Commands with parameters
|
||||
|
||||
We've also long experimented with the idea of commands having parameters that
|
||||
can be filled in by the user. These would be commands that take a couple
|
||||
lightweight inputs, so that the use can input them more natively than an
|
||||
adaptive card.
|
||||
|
||||
Previous drafts included a new type of `ICommand` ala
|
||||
`IInvokableWithParameters`. However, these ran into edge cases:
|
||||
* Where are the parameters displayed? In the search box? On the item?
|
||||
* What happens if a context menu command needs parameters?
|
||||
* Does the _page_ have parameters?
|
||||
|
||||
none of which were trivially solvable by having the parameters on the command.
|
||||
|
||||
Instead, we can leverage the concept of a "rich search" experience to provide
|
||||
that lightweight parameter input method.
|
||||
|
||||
We'll add a new type of page, called a `RichSearchPage`. It's a list page, but
|
||||
with a rich search box at the top.
|
||||
|
||||
```c#
|
||||
interface IRichSearchPage requires IListPage {
|
||||
IRichSearch Search { get; };
|
||||
};
|
||||
```
|
||||
|
||||
This lets the user activate the command that needs parameters, and go straight
|
||||
into the rich input page. That page will act like it's _just_ the command the
|
||||
user "invoked", and will let us display additional inputs to the user.
|
||||
|
||||
#### Parameters Example
|
||||
|
||||
Now, lets say you had a command like "Create a note \${title} in \${folder}".
|
||||
`title` is a string input, and `folder` is a static list of folders.
|
||||
|
||||
The extension author can then define a `RichSearchPage` with a `IRichSearch`
|
||||
that has four tokens in it:
|
||||
* A `ILabelToken` for "Create a note"
|
||||
* A `IStringInputToken` for the `title`
|
||||
* A `ILabelToken` for "in"
|
||||
* A `IStaticListToken` for the `folder`, where the items are possible folders
|
||||
|
||||
Then, when the user hits <kbd>↲</kbd>, we gather up all the tokens, and we can
|
||||
reference them in the command. As an example, here's the `CreateNoteCommand`,
|
||||
which implements the `IRichSearchPage` interface:
|
||||
|
||||
The list page can always change it's results based on the user's input. In our
|
||||
case, we'll listen for the value of the last token to be set. When it is, we can
|
||||
then display the final list item with our fully formed command for the user to
|
||||
invoke.
|
||||
|
||||
```csharp
|
||||
class CreateNoteCommand : IRichSearchPage {
|
||||
public string Name => "Create a note";
|
||||
public string Id => "create_note";
|
||||
public IIconInfo Icon => null;
|
||||
|
||||
public IRichSearch Search { get; }
|
||||
private StringInputToken _titleToken;
|
||||
private NotesFolderToken _folderToken;
|
||||
|
||||
private IListItem? _createNoteItem;
|
||||
|
||||
public CreateNoteCommand() {
|
||||
Search = new RichSearch();
|
||||
|
||||
_titleToken = new StringInputToken("title", "Title of the note");
|
||||
_folderToken = new NotesFolderToken("folder", "Select a folder");
|
||||
|
||||
Search.SearchTokens = [
|
||||
new LabelToken("Create a note"),
|
||||
_titleToken,
|
||||
new LabelToken("in"),
|
||||
_folderToken
|
||||
];
|
||||
|
||||
_folderToken.OnSelectedItemChanged += (sender, e) => {
|
||||
// Update the command with the selected folder
|
||||
UpdateCommand();
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateCommand() {
|
||||
_createNoteItem = new ListItem() {
|
||||
Title = _titleToken.Value,
|
||||
Subtitle = _folderToken.SelectedItem?.Title,
|
||||
Command = new CreateNoteCommand(title: _titleToken.Value, folder: _folderToken.SelectedItem.Value)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2053,99 +2053,7 @@ This addenda is broken into multiple draft specs currently. These represent diff
|
||||
* [Rich Search Box](./RichSearchBox-draft-A.md)
|
||||
* [Prefix Search](./PrefixSearch-draft-B.md)
|
||||
* [ZWSP tokens](./PlainRichSearch-draft-C.md)
|
||||
|
||||
### Commands with parameters
|
||||
|
||||
We've also long experimented with the idea of commands having parameters that
|
||||
can be filled in by the user. These would be commands that take a couple
|
||||
lightweight inputs, so that the use can input them more natively than an
|
||||
adaptive card.
|
||||
|
||||
Previous drafts included a new type of `ICommand` ala
|
||||
`IInvokableWithParameters`. However, these ran into edge cases:
|
||||
* Where are the parameters displayed? In the search box? On the item?
|
||||
* What happens if a context menu command needs parameters?
|
||||
* Does the _page_ have parameters?
|
||||
|
||||
none of which were trivially solvable by having the parameters on the command.
|
||||
|
||||
Instead, we can leverage the concept of a "rich search" experience to provide
|
||||
that lightweight parameter input method.
|
||||
|
||||
We'll add a new type of page, called a `RichSearchPage`. It's a list page, but
|
||||
with a rich search box at the top.
|
||||
|
||||
```c#
|
||||
interface IRichSearchPage requires IListPage {
|
||||
IRichSearch Search { get; };
|
||||
};
|
||||
```
|
||||
|
||||
This lets the user activate the command that needs parameters, and go straight
|
||||
into the rich input page. That page will act like it's _just_ the command the
|
||||
user "invoked", and will let us display additional inputs to the user.
|
||||
|
||||
#### Parameters Example
|
||||
|
||||
Now, lets say you had a command like "Create a note \${title} in \${folder}".
|
||||
`title` is a string input, and `folder` is a static list of folders.
|
||||
|
||||
The extension author can then define a `RichSearchPage` with a `IRichSearch`
|
||||
that has four tokens in it:
|
||||
* A `ILabelToken` for "Create a note"
|
||||
* A `IStringInputToken` for the `title`
|
||||
* A `ILabelToken` for "in"
|
||||
* A `IStaticListToken` for the `folder`, where the items are possible folders
|
||||
|
||||
Then, when the user hits <kbd>↲</kbd>, we gather up all the tokens, and we can
|
||||
reference them in the command. As an example, here's the `CreateNoteCommand`,
|
||||
which implements the `IRichSearchPage` interface:
|
||||
|
||||
The list page can always change it's results based on the user's input. In our
|
||||
case, we'll listen for the value of the last token to be set. When it is, we can
|
||||
then display the final list item with our fully formed command for the user to
|
||||
invoke.
|
||||
|
||||
```csharp
|
||||
class CreateNoteCommand : IRichSearchPage {
|
||||
public string Name => "Create a note";
|
||||
public string Id => "create_note";
|
||||
public IIconInfo Icon => null;
|
||||
|
||||
public IRichSearch Search { get; }
|
||||
private StringInputToken _titleToken;
|
||||
private NotesFolderToken _folderToken;
|
||||
|
||||
private IListItem? _createNoteItem;
|
||||
|
||||
public CreateNoteCommand() {
|
||||
Search = new RichSearch();
|
||||
|
||||
_titleToken = new StringInputToken("title", "Title of the note");
|
||||
_folderToken = new NotesFolderToken("folder", "Select a folder");
|
||||
|
||||
Search.SearchTokens = [
|
||||
new LabelToken("Create a note"),
|
||||
_titleToken,
|
||||
new LabelToken("in"),
|
||||
_folderToken
|
||||
];
|
||||
|
||||
_folderToken.OnSelectedItemChanged += (sender, e) => {
|
||||
// Update the command with the selected folder
|
||||
UpdateCommand();
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateCommand() {
|
||||
_createNoteItem = new ListItem() {
|
||||
Title = _titleToken.Value,
|
||||
Subtitle = _folderToken.SelectedItem?.Title,
|
||||
Command = new CreateNoteCommand(title: _titleToken.Value, folder: _folderToken.SelectedItem.Value)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
* [Parameter Page](./ParameterPage-draft-D.md)
|
||||
|
||||
|
||||
### Miscellaneous notes
|
||||
|
||||
Reference in New Issue
Block a user