mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 04:00:02 +01:00
Add support for showing status messages in the host (#281)
This PR is a ton of _plumbing_. UX-wise, this is very rough. What's more important in this PR is the broad wiring this does, to connect individual pages with the `IExtension` that's hosting them. `CommandPaletteHost` is the important new class that we're introducing. This is the class that implements the `IExtensionHost` interface, and is the one by which extensions can use to log messages back to the host. There's both: * A singleton instance of the `CommandPaletteHost`, which represents all global state, * per-extension instances of the `CommandPaletteHost`, which allows us to know which extension a message came from. When we fetch a command provider, we'll create a new `CommandPaletteHost` for that extension, and connect the extension to that instance. * Log messages from an extension go to the global list of messages, so those go to the global instance's list of `LogMessageViewModel`s * When an extension writes status messages, we'll add the messages to _that extension's_ `CommandPaletteHost`. * The `PageContext` is aware of the `CommandPaletteHost`, so it can now retrieve information about the hosting extension for that page. Since all pages for an extension share a single `CommandPaletteHost`, status messages can be shown across all the pages in that extension's context, then hidden when the user leaves that context. This also does part of #253, because now we have a `TopLevelCommandWrapper` AND a `TopLevelCommandItemWrapper`, separately. That lets us store the `CommandPaletteHost` in the `TopLevelCommandWrapper`, which we need so that when we activate a top-level command, we can fetch the extension host out of it and give it to the pages that follow. Also included is the "single builtin command provider" which is also in #264, because it's kinda insane to have things like "Quit", "Reload extension", "View log", things which are all _core pieces of the palette itself_, each need a separate provider. That's insane. I didn't add support for: * Extensions to hide messages once they're shown * I dunno if `PropChanged`'ing a status message works * I didn't add support for progress bars yet, because it's NOT TRIVIAL to replace the icon of an InfoBar with a progress wheel. What the heck WinUI 😠 * Again, this is Programmer Xaml - we'll need real designers to come around and clean this up --------- Co-authored-by: Mike Griese <zadjii@gmail.com>
This commit is contained in:
@@ -29,7 +29,8 @@ internal sealed partial class SampleListPage : ListPage
|
||||
Text = "Sample Tag",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
new ListItem(new SendMessageCommand()) { Title = "I send messages" },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPalExtensions\$(RootNamespace)</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
5
src/modules/cmdpal/Invoke-XamlFormat.ps1
Normal file
5
src/modules/cmdpal/Invoke-XamlFormat.ps1
Normal file
@@ -0,0 +1,5 @@
|
||||
$gitRoot = git rev-parse --show-toplevel
|
||||
|
||||
# $xamlFilesForStyler = (git ls-files "$gitRoot/**/*.xaml") -join ","
|
||||
$xamlFilesForStyler = (git ls-files "$gitRoot/src/modules/cmdpal/**/*.xaml") -join ","
|
||||
dotnet tool run xstyler -- -c "$gitRoot\src\Settings.XamlStyler" -f "$xamlFilesForStyler"
|
||||
@@ -0,0 +1,119 @@
|
||||
// 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;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class CommandPaletteHost : IExtensionHost
|
||||
{
|
||||
// Static singleton, so that we can access this from anywhere
|
||||
// Post MVVM - this should probably be like, a dependency injection thing.
|
||||
public static CommandPaletteHost Instance { get; } = new();
|
||||
|
||||
private static readonly GlobalLogPageContext _globalLogPageContext = new();
|
||||
|
||||
private ulong _hostHwnd;
|
||||
|
||||
public ulong HostingHwnd => _hostHwnd;
|
||||
|
||||
public string LanguageOverride => string.Empty;
|
||||
|
||||
public static ObservableCollection<LogMessageViewModel> LogMessages { get; } = new();
|
||||
|
||||
public ObservableCollection<StatusMessageViewModel> StatusMessages { get; } = new();
|
||||
|
||||
private readonly IExtensionWrapper? _source;
|
||||
|
||||
public IExtensionWrapper? Extension => _source;
|
||||
|
||||
private CommandPaletteHost()
|
||||
{
|
||||
}
|
||||
|
||||
public CommandPaletteHost(IExtensionWrapper source)
|
||||
{
|
||||
_source = source;
|
||||
}
|
||||
|
||||
public IAsyncAction ShowStatus(IStatusMessage message)
|
||||
{
|
||||
Debug.WriteLine(message.Message);
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
ProcessStatusMessage(message);
|
||||
});
|
||||
|
||||
return Task.CompletedTask.AsAsyncAction();
|
||||
}
|
||||
|
||||
public IAsyncAction HideStatus(IStatusMessage message)
|
||||
{
|
||||
return Task.CompletedTask.AsAsyncAction();
|
||||
}
|
||||
|
||||
public IAsyncAction LogMessage(ILogMessage message)
|
||||
{
|
||||
Debug.WriteLine(message.Message);
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
ProcessLogMessage(message);
|
||||
});
|
||||
|
||||
// We can't just make a LogMessageViewModel : ExtensionObjectViewModel
|
||||
// because we don't necessarily know the page context. Butts.
|
||||
return Task.CompletedTask.AsAsyncAction();
|
||||
}
|
||||
|
||||
public void ProcessLogMessage(ILogMessage message)
|
||||
{
|
||||
var vm = new LogMessageViewModel(message, _globalLogPageContext);
|
||||
vm.SafeInitializePropertiesSynchronous();
|
||||
|
||||
if (_source != null)
|
||||
{
|
||||
vm.ExtensionPfn = _source.PackageFamilyName;
|
||||
}
|
||||
|
||||
Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
LogMessages.Add(vm);
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
_globalLogPageContext.Scheduler);
|
||||
}
|
||||
|
||||
public void ProcessStatusMessage(IStatusMessage message)
|
||||
{
|
||||
var vm = new StatusMessageViewModel(message, _globalLogPageContext);
|
||||
vm.SafeInitializePropertiesSynchronous();
|
||||
|
||||
if (_source != null)
|
||||
{
|
||||
vm.ExtensionPfn = _source.PackageFamilyName;
|
||||
}
|
||||
|
||||
Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
StatusMessages.Add(vm);
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
_globalLogPageContext.Scheduler);
|
||||
}
|
||||
|
||||
public void SetHostHwnd(ulong hostHwnd)
|
||||
{
|
||||
_hostHwnd = hostHwnd;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
@@ -14,7 +16,7 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
private readonly bool isValid;
|
||||
|
||||
private readonly ICommandProvider _commandProvider;
|
||||
private readonly ExtensionObject<ICommandProvider> _commandProvider;
|
||||
|
||||
private readonly IExtensionWrapper? extensionWrapper;
|
||||
|
||||
@@ -22,22 +24,31 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
public IFallbackCommandItem[] FallbackItems { get; private set; } = [];
|
||||
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
|
||||
public IExtensionWrapper? Extension => extensionWrapper;
|
||||
|
||||
public CommandPaletteHost ExtensionHost { get; private set; }
|
||||
|
||||
public event TypedEventHandler<CommandProviderWrapper, ItemsChangedEventArgs>? CommandsChanged;
|
||||
|
||||
public string Id { get; private set; } = string.Empty;
|
||||
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
|
||||
public IconInfoViewModel Icon { get; private set; } = new(null);
|
||||
|
||||
public string ProviderId => $"{extensionWrapper?.PackageFamilyName ?? string.Empty}/{Id}";
|
||||
|
||||
public IExtensionWrapper? Extension => extensionWrapper;
|
||||
|
||||
public CommandProviderWrapper(ICommandProvider provider)
|
||||
{
|
||||
_commandProvider = provider;
|
||||
_commandProvider.ItemsChanged += CommandProvider_ItemsChanged;
|
||||
// This ctor is only used for in-proc builtin commands. So the Unsafe!
|
||||
// calls are pretty dang safe actually.
|
||||
_commandProvider = new(provider);
|
||||
|
||||
// Hook the extension back into us
|
||||
ExtensionHost = CommandPaletteHost.Instance;
|
||||
_commandProvider.Unsafe!.InitializeWithHost(ExtensionHost);
|
||||
|
||||
_commandProvider.Unsafe!.ItemsChanged += CommandProvider_ItemsChanged;
|
||||
|
||||
isValid = true;
|
||||
Id = provider.Id;
|
||||
@@ -49,6 +60,7 @@ public sealed class CommandProviderWrapper
|
||||
public CommandProviderWrapper(IExtensionWrapper extension)
|
||||
{
|
||||
extensionWrapper = extension;
|
||||
ExtensionHost = new CommandPaletteHost(extension);
|
||||
if (!extensionWrapper.IsRunning())
|
||||
{
|
||||
throw new ArgumentException("You forgot to start the extension. This is a coding error - make sure to call StartExtensionAsync");
|
||||
@@ -61,14 +73,25 @@ public sealed class CommandProviderWrapper
|
||||
throw new ArgumentException("extension didn't actually implement ICommandProvider");
|
||||
}
|
||||
|
||||
_commandProvider = provider;
|
||||
_commandProvider = new(provider);
|
||||
|
||||
try
|
||||
{
|
||||
_commandProvider.ItemsChanged += CommandProvider_ItemsChanged;
|
||||
var model = _commandProvider.Unsafe!;
|
||||
|
||||
// Hook the extension back into us
|
||||
model.InitializeWithHost(ExtensionHost);
|
||||
model.ItemsChanged += CommandProvider_ItemsChanged;
|
||||
|
||||
DisplayName = model.DisplayName;
|
||||
|
||||
isValid = true;
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine("Failed to initialize CommandProvider for extension.");
|
||||
Debug.WriteLine($"Extension was {extensionWrapper!.PackageFamilyName}");
|
||||
Debug.WriteLine(e);
|
||||
}
|
||||
|
||||
isValid = true;
|
||||
@@ -81,17 +104,31 @@ public sealed class CommandProviderWrapper
|
||||
return;
|
||||
}
|
||||
|
||||
var t = new Task<ICommandItem[]>(_commandProvider.TopLevelCommands);
|
||||
t.Start();
|
||||
var commands = await t.ConfigureAwait(false);
|
||||
ICommandItem[]? commands = null;
|
||||
IFallbackCommandItem[]? fallbacks = null;
|
||||
|
||||
// On a BG thread here
|
||||
var fallbacks = _commandProvider.FallbackCommands();
|
||||
try
|
||||
{
|
||||
var model = _commandProvider.Unsafe!;
|
||||
|
||||
Id = _commandProvider.Id;
|
||||
DisplayName = _commandProvider.DisplayName;
|
||||
Icon = new(_commandProvider.Icon);
|
||||
Icon.InitializeProperties();
|
||||
var t = new Task<ICommandItem[]>(model.TopLevelCommands);
|
||||
t.Start();
|
||||
commands = await t.ConfigureAwait(false);
|
||||
|
||||
// On a BG thread here
|
||||
fallbacks = model.FallbackCommands();
|
||||
|
||||
Id = model.Id;
|
||||
DisplayName = model.DisplayName;
|
||||
Icon = new(model.Icon);
|
||||
Icon.InitializeProperties();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine("Failed to load commands from extension");
|
||||
Debug.WriteLine($"Extension was {extensionWrapper!.PackageFamilyName}");
|
||||
Debug.WriteLine(e);
|
||||
}
|
||||
|
||||
if (commands != null)
|
||||
{
|
||||
|
||||
@@ -16,16 +16,18 @@ public partial class BuiltInsCommandProvider : CommandProvider
|
||||
private readonly OpenSettingsCommand openSettings = new();
|
||||
private readonly QuitAction quitAction = new();
|
||||
private readonly FallbackReloadItem _fallbackReloadItem = new();
|
||||
private readonly FallbackLogItem _fallbackLogItem = new();
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() =>
|
||||
[
|
||||
new CommandItem(openSettings) { Subtitle = "Open Command Palette settings" }
|
||||
new CommandItem(openSettings) { Subtitle = "Open Command Palette settings" },
|
||||
];
|
||||
|
||||
public override IFallbackCommandItem[] FallbackCommands() =>
|
||||
[
|
||||
new FallbackCommandItem(quitAction) { Subtitle = "Exit Command Palette" },
|
||||
_fallbackReloadItem,
|
||||
_fallbackLogItem,
|
||||
];
|
||||
|
||||
public BuiltInsCommandProvider()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
internal sealed partial class FallbackLogItem : FallbackCommandItem
|
||||
{
|
||||
private readonly LogMessagesPage _logMessagesPage;
|
||||
|
||||
public FallbackLogItem()
|
||||
: base(new LogMessagesPage())
|
||||
{
|
||||
_logMessagesPage = (LogMessagesPage)Command!;
|
||||
Title = string.Empty;
|
||||
_logMessagesPage.Name = string.Empty;
|
||||
Subtitle = "View log messages";
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
_logMessagesPage.Name = query.StartsWith('l') ? "View log" : string.Empty;
|
||||
Title = _logMessagesPage.Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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.Specialized;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
|
||||
public partial class LogMessagesPage : ListPage
|
||||
{
|
||||
private readonly List<IListItem> _listItems = new();
|
||||
|
||||
public LogMessagesPage()
|
||||
{
|
||||
Name = "View log";
|
||||
Title = "Log";
|
||||
Icon = new("\uE8FD");
|
||||
CommandPaletteHost.LogMessages.CollectionChanged += LogMessages_CollectionChanged;
|
||||
}
|
||||
|
||||
private void LogMessages_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
|
||||
{
|
||||
foreach (var item in e.NewItems)
|
||||
{
|
||||
if (item is LogMessageViewModel logMessageViewModel)
|
||||
{
|
||||
var li = new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = logMessageViewModel.Message,
|
||||
Subtitle = logMessageViewModel.ExtensionPfn,
|
||||
};
|
||||
_listItems.Insert(0, li);
|
||||
}
|
||||
}
|
||||
|
||||
RaiseItemsChanged(_listItems.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
return _listItems.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
private readonly ObservableCollection<TopLevelCommandWrapper> _commands;
|
||||
private readonly ObservableCollection<TopLevelCommandItemWrapper> _commands;
|
||||
|
||||
private IEnumerable<IListItem>? _filteredItems;
|
||||
|
||||
@@ -161,7 +161,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
|
||||
var isFallback = false;
|
||||
if (topLevelOrAppItem is TopLevelCommandWrapper toplevel)
|
||||
if (topLevelOrAppItem is TopLevelCommandItemWrapper toplevel)
|
||||
{
|
||||
isFallback = toplevel.IsFallback;
|
||||
}
|
||||
|
||||
@@ -19,19 +19,24 @@ public abstract partial class ExtensionObjectViewModel : ObservableObject
|
||||
{
|
||||
var t = new Task(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeProperties();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PageContext.ShowException(ex);
|
||||
}
|
||||
SafeInitializePropertiesSynchronous();
|
||||
});
|
||||
t.Start();
|
||||
await t;
|
||||
}
|
||||
|
||||
public void SafeInitializePropertiesSynchronous()
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeProperties();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PageContext.ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void InitializeProperties();
|
||||
|
||||
protected void UpdateProperty(string propertyName) =>
|
||||
|
||||
@@ -19,8 +19,8 @@ public partial class FormsPageViewModel : PageViewModel
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
public FormsPageViewModel(IFormPage model, TaskScheduler scheduler)
|
||||
: base(model, scheduler)
|
||||
public FormsPageViewModel(IFormPage model, TaskScheduler scheduler, CommandPaletteHost host)
|
||||
: base(model, scheduler, host)
|
||||
{
|
||||
_model = new(model);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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 class GlobalLogPageContext : IPageContext
|
||||
{
|
||||
public TaskScheduler Scheduler { get; private init; }
|
||||
|
||||
public void ShowException(Exception ex, string? extensionHint)
|
||||
{ /*do nothing*/
|
||||
}
|
||||
|
||||
public GlobalLogPageContext()
|
||||
{
|
||||
Scheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,8 @@ public partial class ListViewModel : PageViewModel
|
||||
|
||||
private bool _isDynamic;
|
||||
|
||||
public ListViewModel(IListPage model, TaskScheduler scheduler)
|
||||
: base(model, scheduler)
|
||||
public ListViewModel(IListPage model, TaskScheduler scheduler, CommandPaletteHost host)
|
||||
: base(model, scheduler, host)
|
||||
{
|
||||
_model = new(model);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class LoadingPageViewModel : PageViewModel
|
||||
{
|
||||
public LoadingPageViewModel(IPage? model, TaskScheduler scheduler)
|
||||
: base(model, scheduler)
|
||||
: base(model, scheduler, CommandPaletteHost.Instance)
|
||||
{
|
||||
ModelIsLoading = true;
|
||||
IsInitialized = false;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class LogMessageViewModel : ExtensionObjectViewModel
|
||||
{
|
||||
private readonly ExtensionObject<ILogMessage> _model;
|
||||
|
||||
public string Message { get; private set; } = string.Empty;
|
||||
|
||||
public string ExtensionPfn { get; set; } = string.Empty;
|
||||
|
||||
public LogMessageViewModel(ILogMessage message, IPageContext context)
|
||||
: base(context)
|
||||
{
|
||||
_model = new(message);
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
Message = model.Message;
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@ public partial class MarkdownPageViewModel : PageViewModel
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
public MarkdownPageViewModel(IMarkdownPage model, TaskScheduler scheduler)
|
||||
: base(model, scheduler)
|
||||
public MarkdownPageViewModel(IMarkdownPage model, TaskScheduler scheduler, CommandPaletteHost host)
|
||||
: base(model, scheduler, host)
|
||||
{
|
||||
_model = new(model);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
@@ -31,6 +32,17 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
[ObservableProperty]
|
||||
public partial string Filter { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial CommandPaletteHost ExtensionHost { get; private set; }
|
||||
|
||||
public bool HasStatusMessage => MostRecentStatusMessage != null;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasStatusMessage))]
|
||||
public partial StatusMessageViewModel? MostRecentStatusMessage { get; private set; } = null;
|
||||
|
||||
public ObservableCollection<StatusMessageViewModel> StatusMessages => ExtensionHost.StatusMessages;
|
||||
|
||||
// These are properties that are "observable" from the extension object
|
||||
// itself, in the sense that they get raised by PropChanged events from the
|
||||
// extension. However, we don't want to actually make them
|
||||
@@ -47,13 +59,35 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
|
||||
public IconInfoViewModel Icon { get; protected set; }
|
||||
|
||||
public PageViewModel(IPage? model, TaskScheduler scheduler)
|
||||
public PageViewModel(IPage? model, TaskScheduler scheduler, CommandPaletteHost extensionHost)
|
||||
: base(null)
|
||||
{
|
||||
_pageModel = new(model);
|
||||
Scheduler = scheduler;
|
||||
PageContext = this;
|
||||
ExtensionHost = extensionHost;
|
||||
Icon = new(null);
|
||||
|
||||
ExtensionHost.StatusMessages.CollectionChanged += StatusMessages_CollectionChanged;
|
||||
UpdateHasStatusMessage();
|
||||
}
|
||||
|
||||
private void StatusMessages_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateHasStatusMessage();
|
||||
}
|
||||
|
||||
private void UpdateHasStatusMessage()
|
||||
{
|
||||
if (ExtensionHost.StatusMessages.Any())
|
||||
{
|
||||
var last = ExtensionHost.StatusMessages.Last();
|
||||
MostRecentStatusMessage = last;
|
||||
}
|
||||
else
|
||||
{
|
||||
MostRecentStatusMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
//// Run on background thread from ListPage.xaml.cs
|
||||
|
||||
@@ -36,7 +36,7 @@ public partial class SettingsViewModel : PageViewModel
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
|
||||
|
||||
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)
|
||||
: base(null, scheduler)
|
||||
: base(null, scheduler, CommandPaletteHost.Instance)
|
||||
{
|
||||
_settings = settings;
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
||||
{
|
||||
private readonly ExtensionObject<IStatusMessage> _model;
|
||||
|
||||
public string Message { get; private set; } = string.Empty;
|
||||
|
||||
public MessageState State { get; private set; } = MessageState.Info;
|
||||
|
||||
public string ExtensionPfn { get; set; } = string.Empty;
|
||||
|
||||
public StatusMessageViewModel(IStatusMessage message, IPageContext context)
|
||||
: base(context)
|
||||
{
|
||||
_model = new(message);
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
Message = model.Message;
|
||||
State = model.State;
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
private void Model_PropChanged(object sender, PropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
FetchProperty(args.PropertyName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PageContext.ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this._model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Message):
|
||||
this.Message = model.Message;
|
||||
break;
|
||||
case nameof(State):
|
||||
this.State = model.State;
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction of a top-level command. Currently owns just a live ICommandItem
|
||||
/// from an extension (or in-proc command provider), but in the future will
|
||||
/// also support stub top-level items.
|
||||
/// </summary>
|
||||
public partial class TopLevelCommandItemWrapper : ListItem
|
||||
{
|
||||
public ExtensionObject<ICommandItem> Model { get; }
|
||||
|
||||
public bool IsFallback { get; private set; }
|
||||
|
||||
public string Id { get; private set; } = string.Empty;
|
||||
|
||||
// public override ICommand? Command { get => base.Command; set => base.Command = value; }
|
||||
private readonly TopLevelCommandWrapper _topLevelCommand;
|
||||
|
||||
public CommandPaletteHost? ExtensionHost { get => _topLevelCommand.ExtensionHost; set => _topLevelCommand.ExtensionHost = value; }
|
||||
|
||||
public TopLevelCommandItemWrapper(ExtensionObject<ICommandItem> commandItem, bool isFallback)
|
||||
: base(new TopLevelCommandWrapper(commandItem.Unsafe?.Command ?? new NoOpCommand()))
|
||||
{
|
||||
_topLevelCommand = (TopLevelCommandWrapper)this.Command!;
|
||||
|
||||
IsFallback = isFallback;
|
||||
|
||||
// TODO: In reality, we should do an async fetch when we're created
|
||||
// from an extension object. Probably have an
|
||||
// `static async Task<TopLevelCommandWrapper> FromExtension(ExtensionObject<ICommandItem>)`
|
||||
// or a
|
||||
// `async Task PromoteStub(ExtensionObject<ICommandItem>)`
|
||||
Model = commandItem;
|
||||
try
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_topLevelCommand.UnsafeInitializeProperties();
|
||||
|
||||
Id = _topLevelCommand.Id;
|
||||
|
||||
Title = model.Title;
|
||||
Subtitle = model.Subtitle;
|
||||
Icon = model.Icon;
|
||||
MoreCommands = model.MoreCommands;
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
_topLevelCommand.PropChanged += Model_PropChanged;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void Model_PropChanged(object sender, PropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var propertyName = args.PropertyName;
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(_topLevelCommand.Name):
|
||||
case nameof(Title):
|
||||
this.Title = model.Title;
|
||||
break;
|
||||
case nameof(Subtitle):
|
||||
this.Subtitle = model.Subtitle;
|
||||
break;
|
||||
case nameof(Icon):
|
||||
var listIcon = model.Icon;
|
||||
Icon = model.Icon;
|
||||
break;
|
||||
|
||||
// TODO! MoreCommands array, which needs to also raise HasMoreCommands
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void TryUpdateFallbackText(string newQuery)
|
||||
{
|
||||
if (!IsFallback)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model is IFallbackCommandItem fallback)
|
||||
{
|
||||
fallback.FallbackHandler.UpdateQuery(newQuery);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
|
||||
}
|
||||
|
||||
public ObservableCollection<TopLevelCommandWrapper> TopLevelCommands { get; set; } = [];
|
||||
public ObservableCollection<TopLevelCommandItemWrapper> TopLevelCommands { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoading { get; private set; } = true;
|
||||
@@ -64,12 +64,20 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
{
|
||||
foreach (var i in commandProvider.TopLevelItems)
|
||||
{
|
||||
TopLevelCommands.Add(new(new(i), false));
|
||||
TopLevelCommandItemWrapper wrapper = new(new(i), false)
|
||||
{
|
||||
ExtensionHost = commandProvider.ExtensionHost,
|
||||
};
|
||||
TopLevelCommands.Add(wrapper);
|
||||
}
|
||||
|
||||
foreach (var i in commandProvider.FallbackItems)
|
||||
{
|
||||
TopLevelCommands.Add(new(new(i), true));
|
||||
TopLevelCommandItemWrapper wrapper = new(new(i), true)
|
||||
{
|
||||
ExtensionHost = commandProvider.ExtensionHost,
|
||||
};
|
||||
TopLevelCommands.Add(wrapper);
|
||||
}
|
||||
},
|
||||
CancellationToken.None,
|
||||
@@ -99,8 +107,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
{
|
||||
// Work on a clone of the list, so that we can just do one atomic
|
||||
// update to the actual observable list at the end
|
||||
List<TopLevelCommandWrapper> clone = [.. TopLevelCommands];
|
||||
List<TopLevelCommandWrapper> newItems = [];
|
||||
List<TopLevelCommandItemWrapper> clone = [.. TopLevelCommands];
|
||||
List<TopLevelCommandItemWrapper> newItems = [];
|
||||
var startIndex = -1;
|
||||
var firstCommand = sender.TopLevelItems[0];
|
||||
var commandsToRemove = sender.TopLevelItems.Length + sender.FallbackItems.Length;
|
||||
@@ -202,7 +210,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
return true;
|
||||
}
|
||||
|
||||
public TopLevelCommandWrapper? LookupCommand(string id)
|
||||
public TopLevelCommandItemWrapper? LookupCommand(string id)
|
||||
{
|
||||
foreach (var command in TopLevelCommands)
|
||||
{
|
||||
|
||||
@@ -3,58 +3,42 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction of a top-level command. Currently owns just a live ICommandItem
|
||||
/// from an extension (or in-proc command provider), but in the future will
|
||||
/// also support stub top-level items.
|
||||
/// </summary>
|
||||
public partial class TopLevelCommandWrapper : ListItem
|
||||
public partial class TopLevelCommandWrapper : ICommand
|
||||
{
|
||||
public ExtensionObject<ICommandItem> Model { get; }
|
||||
private readonly ExtensionObject<ICommand> _command;
|
||||
|
||||
private readonly bool _isFallback;
|
||||
public event TypedEventHandler<object, PropChangedEventArgs>? PropChanged;
|
||||
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
|
||||
public string Id { get; private set; } = string.Empty;
|
||||
|
||||
public bool IsFallback => _isFallback;
|
||||
public IconInfo Icon { get; private set; } = new(null);
|
||||
|
||||
public TopLevelCommandWrapper(ExtensionObject<ICommandItem> commandItem, bool isFallback)
|
||||
: base(commandItem.Unsafe?.Command ?? new NoOpCommand())
|
||||
public ICommand Command => _command.Unsafe!;
|
||||
|
||||
public CommandPaletteHost? ExtensionHost { get; set; }
|
||||
|
||||
public TopLevelCommandWrapper(ICommand command)
|
||||
{
|
||||
_isFallback = isFallback;
|
||||
_command = new(command);
|
||||
}
|
||||
|
||||
// TODO: In reality, we should do an async fetch when we're created
|
||||
// from an extension object. Probably have an
|
||||
// `static async Task<TopLevelCommandWrapper> FromExtension(ExtensionObject<ICommandItem>)`
|
||||
// or a
|
||||
// `async Task PromoteStub(ExtensionObject<ICommandItem>)`
|
||||
Model = commandItem;
|
||||
try
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
public void UnsafeInitializeProperties()
|
||||
{
|
||||
var model = _command.Unsafe!;
|
||||
|
||||
Id = model.Command?.Id ?? string.Empty;
|
||||
Name = model.Name;
|
||||
Id = model.Id;
|
||||
Icon = model.Icon;
|
||||
|
||||
Title = model.Title;
|
||||
Subtitle = model.Subtitle;
|
||||
Icon = model.Icon;
|
||||
MoreCommands = model.MoreCommands;
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(ex);
|
||||
}
|
||||
model.PropChanged += Model_PropChanged;
|
||||
model.PropChanged += this.PropChanged;
|
||||
}
|
||||
|
||||
private void Model_PropChanged(object sender, PropChangedEventArgs args)
|
||||
@@ -62,7 +46,7 @@ public partial class TopLevelCommandWrapper : ListItem
|
||||
try
|
||||
{
|
||||
var propertyName = args.PropertyName;
|
||||
var model = Model.Unsafe;
|
||||
var model = _command.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
@@ -70,45 +54,19 @@ public partial class TopLevelCommandWrapper : ListItem
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Title):
|
||||
this.Title = model.Title;
|
||||
break;
|
||||
case nameof(Subtitle):
|
||||
this.Subtitle = model.Subtitle;
|
||||
case nameof(Name):
|
||||
this.Name = model.Name;
|
||||
break;
|
||||
case nameof(Icon):
|
||||
var listIcon = model.Icon;
|
||||
Icon = model.Icon;
|
||||
break;
|
||||
|
||||
// TODO! MoreCommands array, which needs to also raise HasMoreCommands
|
||||
}
|
||||
|
||||
PropChanged?.Invoke(this, args);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// This is only ever called on a background thread, by
|
||||
// MainListPage::UpdateSearchText, which is already running in the
|
||||
// background. So x-proc calls we do in here are okay.
|
||||
public void TryUpdateFallbackText(string newQuery)
|
||||
{
|
||||
if (!_isFallback)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model is IFallbackCommandItem fallback)
|
||||
{
|
||||
fallback.FallbackHandler.UpdateQuery(newQuery);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<cmdpalUI:MessageStateToSeverityConverter x:Key="MessageStateToSeverityConverter" />
|
||||
|
||||
<StackLayout
|
||||
x:Name="VerticalStackLayout"
|
||||
Orientation="Vertical"
|
||||
Spacing="4" />
|
||||
|
||||
<!-- Template for actions in the mode actions dropdown button -->
|
||||
<DataTemplate x:Key="ContextMenuViewModelTemplate" x:DataType="viewmodels:CommandContextItemViewModel">
|
||||
<ListViewItem KeyDown="ActionListViewItem_KeyDown" Tapped="ActionListViewItem_Tapped">
|
||||
@@ -55,15 +62,49 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
Width="16"
|
||||
Height="16"
|
||||
<Grid
|
||||
x:Name="IconRoot"
|
||||
Margin="8,0,0,0"
|
||||
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
SourceKey="{x:Bind CurrentPageViewModel.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
Tapped="PageIcon_Tapped">
|
||||
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
Width="16"
|
||||
Height="16"
|
||||
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
SourceKey="{x:Bind CurrentPageViewModel.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<InfoBadge Visibility="{x:Bind CurrentPageViewModel.HasStatusMessage, Mode=OneWay}" Value="{x:Bind CurrentPageViewModel.StatusMessages.Count, Mode=OneWay}" />
|
||||
<Grid.ContextFlyout>
|
||||
<Flyout x:Name="StatusMessagesFlyout" Placement="TopEdgeAlignedLeft">
|
||||
<ItemsRepeater
|
||||
x:Name="MessagesDropdown"
|
||||
Margin="-8"
|
||||
ItemsSource="{x:Bind CurrentPageViewModel.StatusMessages, Mode=OneWay}"
|
||||
Layout="{StaticResource VerticalStackLayout}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewmodels:StatusMessageViewModel">
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
CornerRadius="0">
|
||||
<InfoBar
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="{x:Bind Message, Mode=OneWay}"
|
||||
Severity="{x:Bind State, Mode=OneWay, Converter={StaticResource MessageStateToSeverityConverter}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</Flyout>
|
||||
</Grid.ContextFlyout>
|
||||
</Grid>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
|
||||
@@ -103,4 +103,14 @@ public sealed partial class ActionBar : UserControl,
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
|
||||
private void SecondaryButton_Tapped(object sender, TappedRoutedEventArgs e) =>
|
||||
WeakReferenceMessenger.Default.Send<ActivateSecondaryCommandMessage>();
|
||||
|
||||
private void PageIcon_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
if (CurrentPageViewModel?.StatusMessages.Count > 0)
|
||||
{
|
||||
StatusMessagesFlyout.ShowAt(
|
||||
placementTarget: IconRoot,
|
||||
showOptions: new FlyoutShowOptions() { ShowMode = FlyoutShowMode.Standard });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ public sealed partial class MainWindow : Window,
|
||||
InitializeComponent();
|
||||
|
||||
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
|
||||
CommandPaletteHost.Instance.SetHostHwnd((ulong)_hwnd.Value);
|
||||
|
||||
PositionCentered();
|
||||
SetAcrylic();
|
||||
|
||||
@@ -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.Extensions;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
public partial class MessageStateToSeverityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is MessageState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case MessageState.Info:
|
||||
return InfoBarSeverity.Informational;
|
||||
case MessageState.Success:
|
||||
return InfoBarSeverity.Success;
|
||||
case MessageState.Warning:
|
||||
return InfoBarSeverity.Warning;
|
||||
case MessageState.Error:
|
||||
return InfoBarSeverity.Error;
|
||||
}
|
||||
}
|
||||
|
||||
return InfoBarSeverity.Informational;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
|
||||
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
@@ -20,11 +21,16 @@
|
||||
x:Key="StringNotEmptyToVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
|
||||
|
||||
<cmdpalUI:MessageStateToSeverityConverter x:Key="MessageStateToSeverityConverter" />
|
||||
|
||||
<converters:BoolToVisibilityConverter
|
||||
x:Key="BoolToInvertedVisibilityConverter"
|
||||
FalseValue="Visible"
|
||||
TrueValue="Collapsed" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</Page.Resources>
|
||||
|
||||
@@ -212,6 +218,31 @@
|
||||
</ScrollView>
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
Horrifying: You may ask yourself - why is there a Background on this InfoBar?
|
||||
|
||||
Well, as it turns out, the Informational InfoBar has a transparent
|
||||
background. It just cannot be bothered. So, we need to manually give
|
||||
it one to actually obscure the text beneath it. And you can't just give
|
||||
the InfoBar itself a Background, because then the other Severity's
|
||||
won't get colorized.
|
||||
|
||||
See https://github.com/microsoft/microsoft-ui-xaml/issues/5741
|
||||
-->
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="16,8,16,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}">
|
||||
<InfoBar
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
IsOpen="{x:Bind ViewModel.CurrentPage.HasStatusMessage, Mode=OneWay}"
|
||||
Message="{x:Bind ViewModel.CurrentPage.MostRecentStatusMessage.Message, Mode=OneWay}"
|
||||
Severity="{x:Bind ViewModel.CurrentPage.MostRecentStatusMessage.State, Mode=OneWay, Converter={StaticResource MessageStateToSeverityConverter}}" />
|
||||
</StackPanel>
|
||||
|
||||
<cpcontrols:ActionBar Grid.Row="1" CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" />
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
@@ -20,8 +21,7 @@ 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 ShellPage :
|
||||
Page,
|
||||
public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
IRecipient<NavigateBackMessage>,
|
||||
IRecipient<PerformCommandMessage>,
|
||||
IRecipient<OpenSettingsMessage>,
|
||||
@@ -91,9 +91,24 @@ public sealed partial class ShellPage :
|
||||
// Or the command may be a stub. Future us problem.
|
||||
try
|
||||
{
|
||||
// This could be navigation to another page or invoking of a command, those are our two main branches of logic here.
|
||||
// For different pages, we may construct different view models and navigate to the central frame to different pages,
|
||||
// Otherwise the logic is mostly the same, outside the main page.
|
||||
var host = ViewModel.CurrentPage?.ExtensionHost ?? CommandPaletteHost.Instance;
|
||||
|
||||
if (command is TopLevelCommandWrapper wrapper)
|
||||
{
|
||||
var tlc = wrapper;
|
||||
command = wrapper.Command;
|
||||
host = tlc.ExtensionHost != null ? tlc.ExtensionHost! : host;
|
||||
#if DEBUG
|
||||
if (tlc.ExtensionHost?.Extension != null)
|
||||
{
|
||||
host.ProcessLogMessage(new LogMessage()
|
||||
{
|
||||
Message = $"Activated top-level command from {tlc.ExtensionHost.Extension.ExtensionDisplayName}",
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (command is IPage page)
|
||||
{
|
||||
_ = _queue.TryEnqueue(() =>
|
||||
@@ -106,12 +121,12 @@ public sealed partial class ShellPage :
|
||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
||||
PageViewModel pageViewModel = page switch
|
||||
{
|
||||
IListPage listPage => new ListViewModel(listPage, _mainTaskScheduler)
|
||||
IListPage listPage => new ListViewModel(listPage, _mainTaskScheduler, host)
|
||||
{
|
||||
IsNested = !isMainPage,
|
||||
},
|
||||
IFormPage formsPage => new FormsPageViewModel(formsPage, _mainTaskScheduler),
|
||||
IMarkdownPage markdownPage => new MarkdownPageViewModel(markdownPage, _mainTaskScheduler),
|
||||
IFormPage formsPage => new FormsPageViewModel(formsPage, _mainTaskScheduler, host),
|
||||
IMarkdownPage markdownPage => new MarkdownPageViewModel(markdownPage, _mainTaskScheduler, host),
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ public partial class CommandItem : BaseObservable, ICommandItem
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand? Command
|
||||
public virtual ICommand? Command
|
||||
{
|
||||
get => _command;
|
||||
set
|
||||
|
||||
@@ -15,13 +15,18 @@ public class ExtensionHost
|
||||
_host = host;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fire-and-forget a log message to the Command Palette host app. Since
|
||||
/// the host is in another process, we do this in a try/catch in a
|
||||
/// background thread, as to not block the calling thread, nor explode if
|
||||
/// the host app is gone.
|
||||
/// </summary>
|
||||
/// <param name="message">The log message to send</param>
|
||||
public static void LogMessage(ILogMessage message)
|
||||
{
|
||||
// TODO this feels like bad async
|
||||
if (Host != null)
|
||||
{
|
||||
// really just fire-and-forget
|
||||
new Task(async () =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -30,7 +35,30 @@ public class ExtensionHost
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}).Start();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogMessage(string message)
|
||||
{
|
||||
var logMessage = new LogMessage() { Message = message };
|
||||
LogMessage(logMessage);
|
||||
}
|
||||
|
||||
public static void ShowStatus(IStatusMessage message)
|
||||
{
|
||||
if (Host != null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Host.ShowStatus(message);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
internal sealed partial class SendMessageCommand : InvokableCommand
|
||||
{
|
||||
private static int sentMessages;
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
var kind = MessageState.Info;
|
||||
switch (sentMessages % 4)
|
||||
{
|
||||
case 0: kind = MessageState.Info; break;
|
||||
case 1: kind = MessageState.Success; break;
|
||||
case 2: kind = MessageState.Warning; break;
|
||||
case 3: kind = MessageState.Error; break;
|
||||
}
|
||||
|
||||
var message = new StatusMessage() { Message = $"I am status message no.{sentMessages++}", State = kind };
|
||||
ExtensionHost.ShowStatus(message);
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user