diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleListPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleListPage.cs
index e231663477..2743154d00 100644
--- a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleListPage.cs
+++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleListPage.cs
@@ -29,7 +29,8 @@ internal sealed partial class SampleListPage : ListPage
Text = "Sample Tag",
}
],
- }
+ },
+ new ListItem(new SendMessageCommand()) { Title = "I send messages" },
];
}
}
diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesExtension.csproj b/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesExtension.csproj
index 8063c58c50..ca40ffc8b2 100644
--- a/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesExtension.csproj
+++ b/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesExtension.csproj
@@ -10,6 +10,7 @@
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPalExtensions\$(RootNamespace)
false
false
+ true
diff --git a/src/modules/cmdpal/Invoke-XamlFormat.ps1 b/src/modules/cmdpal/Invoke-XamlFormat.ps1
new file mode 100644
index 0000000000..88dbeb51c2
--- /dev/null
+++ b/src/modules/cmdpal/Invoke-XamlFormat.ps1
@@ -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"
\ No newline at end of file
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs
new file mode 100644
index 0000000000..02be57a5a1
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs
@@ -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 LogMessages { get; } = new();
+
+ public ObservableCollection 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;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs
index c8b7cdd9ab..7e02c09c9b 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs
@@ -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 _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? 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(_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(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)
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs
index 85e44d94eb..301140496d 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs
@@ -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()
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs
new file mode 100644
index 0000000000..3b8510de99
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs
@@ -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;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs
new file mode 100644
index 0000000000..b9c640ac61
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs
@@ -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 _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();
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs
index bbfeba4348..44cb1f7016 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs
@@ -23,7 +23,7 @@ public partial class MainListPage : DynamicListPage,
{
private readonly IServiceProvider _serviceProvider;
- private readonly ObservableCollection _commands;
+ private readonly ObservableCollection _commands;
private IEnumerable? _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;
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ExtensionObjectViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ExtensionObjectViewModel.cs
index e11b75bf14..4c94980068 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ExtensionObjectViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ExtensionObjectViewModel.cs
@@ -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) =>
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FormsPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FormsPageViewModel.cs
index b28fcaa82b..27f91fdb13 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FormsPageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FormsPageViewModel.cs
@@ -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);
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/GlobalLogPageContext.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/GlobalLogPageContext.cs
new file mode 100644
index 0000000000..fde1a36817
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/GlobalLogPageContext.cs
@@ -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();
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
index 18feafcbd7..9311977c9e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
@@ -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);
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LoadingPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LoadingPageViewModel.cs
index 92cd6c82d1..fb732eef06 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LoadingPageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LoadingPageViewModel.cs
@@ -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;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LogMessageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LogMessageViewModel.cs
new file mode 100644
index 0000000000..7b4ed88e30
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LogMessageViewModel.cs
@@ -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 _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;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/MarkdownPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/MarkdownPageViewModel.cs
index bb72b5e1fd..3d134805a2 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/MarkdownPageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/MarkdownPageViewModel.cs
@@ -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);
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
index e263936662..1a9b599d08 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
@@ -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 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
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
index 034e31a07f..a7bfa785c6 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
@@ -36,7 +36,7 @@ public partial class SettingsViewModel : PageViewModel
public ObservableCollection CommandProviders { get; } = [];
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)
- : base(null, scheduler)
+ : base(null, scheduler, CommandPaletteHost.Instance)
{
_settings = settings;
_serviceProvider = serviceProvider;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/StatusMessageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/StatusMessageViewModel.cs
new file mode 100644
index 0000000000..700681f2b7
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/StatusMessageViewModel.cs
@@ -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 _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);
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandItemWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandItemWrapper.cs
new file mode 100644
index 0000000000..f635575142
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandItemWrapper.cs
@@ -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;
+
+///
+/// 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.
+///
+public partial class TopLevelCommandItemWrapper : ListItem
+{
+ public ExtensionObject 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 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 FromExtension(ExtensionObject)`
+ // or a
+ // `async Task PromoteStub(ExtensionObject)`
+ 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)
+ {
+ }
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
index 9abdba5815..513f08e6d6 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
@@ -31,7 +31,7 @@ public partial class TopLevelCommandManager : ObservableObject,
WeakReferenceMessenger.Default.Register(this);
}
- public ObservableCollection TopLevelCommands { get; set; } = [];
+ public ObservableCollection 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 clone = [.. TopLevelCommands];
- List newItems = [];
+ List clone = [.. TopLevelCommands];
+ List 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)
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandWrapper.cs
index b08d3dd095..8a24b6c443 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandWrapper.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandWrapper.cs
@@ -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;
-///
-/// 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.
-///
-public partial class TopLevelCommandWrapper : ListItem
+public partial class TopLevelCommandWrapper : ICommand
{
- public ExtensionObject Model { get; }
+ private readonly ExtensionObject _command;
- private readonly bool _isFallback;
+ public event TypedEventHandler