Create a Microsoft.CmdPal.Core.ViewModels project (#40560)

_targets #40504_ 

Major refactoring for #40113

This moves a large swath of the codebase to a `.Core` project. "Core"
doesn't have any explicit dependencies on "extensions", settings or the
current `MainListPage`. It's just a filterable list of stuff. This
should let us make this component a bit more reusable.

This is half of a PR. As I did this, I noticed a particular bit of code
for TopLevelVViewModels and CommandPaletteHost that was _very rough_.
Solving it in this PR would make "move everything to a new project" much
harder to review. So I'm submitting two PRs simultaneously, so we can
see the changes separately, then merge together.
This commit is contained in:
Mike Griese
2025-07-15 12:21:44 -05:00
committed by GitHub
parent 53bb471449
commit cc16b61eb7
121 changed files with 733 additions and 571 deletions

View File

@@ -4,7 +4,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Messages;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -38,7 +38,8 @@ public partial class AliasManager : ObservableObject
if (topLevelCommand != null)
{
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(topLevelCommand));
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(topLevelCommand.GetPerformCommandMessage());
return true;
}
}

View File

@@ -1,167 +0,0 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandBarViewModel : ObservableObject,
IRecipient<UpdateCommandBarMessage>
{
public ICommandBarContext? SelectedItem
{
get => field;
set
{
if (field != null)
{
field.PropertyChanged -= SelectedItemPropertyChanged;
}
field = value;
SetSelectedItem(value);
OnPropertyChanged(nameof(SelectedItem));
}
}
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasPrimaryCommand))]
public partial CommandItemViewModel? PrimaryCommand { get; set; }
public bool HasPrimaryCommand => PrimaryCommand != null && PrimaryCommand.ShouldBeVisible;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasSecondaryCommand))]
public partial CommandItemViewModel? SecondaryCommand { get; set; }
public bool HasSecondaryCommand => SecondaryCommand != null;
[ObservableProperty]
public partial bool ShouldShowContextMenu { get; set; } = false;
[ObservableProperty]
public partial PageViewModel? CurrentPage { get; set; }
public CommandBarViewModel()
{
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
}
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
private void SetSelectedItem(ICommandBarContext? value)
{
if (value != null)
{
PrimaryCommand = value.PrimaryCommand;
value.PropertyChanged += SelectedItemPropertyChanged;
}
else
{
if (SelectedItem != null)
{
SelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
}
PrimaryCommand = null;
}
UpdateContextItems();
}
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(SelectedItem.HasMoreCommands):
UpdateContextItems();
break;
}
}
private void UpdateContextItems()
{
if (SelectedItem == null)
{
SecondaryCommand = null;
ShouldShowContextMenu = false;
return;
}
SecondaryCommand = SelectedItem.SecondaryCommand;
ShouldShowContextMenu = SelectedItem.MoreCommands
.OfType<CommandContextItemViewModel>()
.Count() > 1;
OnPropertyChanged(nameof(HasSecondaryCommand));
OnPropertyChanged(nameof(SecondaryCommand));
OnPropertyChanged(nameof(ShouldShowContextMenu));
}
// InvokeItemCommand is what this will be in Xaml due to source generator
// this comes in when an item in the list is tapped
// [RelayCommand]
public ContextKeybindingResult InvokeItem(CommandContextItemViewModel item) =>
PerformCommand(item);
// this comes in when the primary button is tapped
public void InvokePrimaryCommand()
{
PerformCommand(PrimaryCommand);
}
// this comes in when the secondary button is tapped
public void InvokeSecondaryCommand()
{
PerformCommand(SecondaryCommand);
}
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var keybindings = SelectedItem?.Keybindings();
if (keybindings != null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (keybindings.TryGetValue(pressedKeyChord, out var matchedItem))
{
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
}
}
return ContextKeybindingResult.Unhandled;
}
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
{
if (command == null)
{
return ContextKeybindingResult.Unhandled;
}
if (command.HasMoreCommands)
{
return ContextKeybindingResult.KeepOpen;
}
else
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
return ContextKeybindingResult.Hide;
}
}
}
public enum ContextKeybindingResult
{
Unhandled,
Hide,
KeepOpen,
}

View File

@@ -1,51 +0,0 @@
// 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 CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
{
private readonly KeyChord nullKeyChord = new(0, 0, 0);
public new ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
public bool IsCritical { get; private set; }
public KeyChord? RequestedShortcut { get; private set; }
public bool HasRequestedShortcut => RequestedShortcut != null && (RequestedShortcut.Value != nullKeyChord);
public override void InitializeProperties()
{
if (IsInitialized)
{
return;
}
base.InitializeProperties();
var contextItem = Model.Unsafe;
if (contextItem == null)
{
return; // throw?
}
IsCritical = contextItem.IsCritical;
// I actually don't think this will ever actually be null, because
// KeyChord is a struct, which isn't nullable in WinRT
if (contextItem.RequestedShortcut != null)
{
RequestedShortcut = new(
contextItem.RequestedShortcut.Modifiers,
contextItem.RequestedShortcut.Vkey,
contextItem.RequestedShortcut.ScanCode);
}
}
}

View File

@@ -1,441 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext
{
public ExtensionObject<ICommandItem> Model => _commandItemModel;
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
private CommandContextItemViewModel? _defaultCommandContextItem;
internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
protected bool IsFastInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.FastInitialized);
protected bool IsInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.Initialized);
protected bool IsSelectedInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.SelectionInitialized);
public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error);
// 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
// [ObservableProperty]s, because PropChanged comes in off the UI thread,
// and ObservableProperty is not smart enough to raise the PropertyChanged
// on the UI thread.
public string Name => Command.Name;
private string _itemTitle = string.Empty;
public string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
public string Subtitle { get; private set; } = string.Empty;
private IconInfoViewModel _listItemIcon = new(null);
public IconInfoViewModel Icon => _listItemIcon.IsSet ? _listItemIcon : Command.Icon;
public CommandViewModel Command { get; private set; }
public List<IContextItemViewModel> MoreCommands { get; private set; } = [];
IEnumerable<IContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
private List<CommandContextItemViewModel> ActualCommands => MoreCommands.OfType<CommandContextItemViewModel>().ToList();
public bool HasMoreCommands => ActualCommands.Count > 0;
public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
public CommandItemViewModel? PrimaryCommand => this;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[0] : null;
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
public List<IContextItemViewModel> AllCommands
{
get
{
List<IContextItemViewModel> l = _defaultCommandContextItem == null ?
new() :
[_defaultCommandContextItem];
l.AddRange(MoreCommands);
return l;
}
}
private static readonly IconInfoViewModel _errorIcon;
static CommandItemViewModel()
{
_errorIcon = new(new IconInfo("\uEA39")); // ErrorBadge
_errorIcon.InitializeProperties();
}
public CommandItemViewModel(ExtensionObject<ICommandItem> item, WeakReference<IPageContext> errorContext)
: base(errorContext)
{
_commandItemModel = item;
Command = new(null, errorContext);
}
public void FastInitializeProperties()
{
if (IsFastInitialized)
{
return;
}
var model = _commandItemModel.Unsafe;
if (model == null)
{
return;
}
Command = new(model.Command, PageContext);
Command.FastInitializeProperties();
_itemTitle = model.Title;
Subtitle = model.Subtitle;
Initialized |= InitializedState.FastInitialized;
}
//// Called from ListViewModel on background thread started in ListPage.xaml.cs
public override void InitializeProperties()
{
if (IsInitialized)
{
return;
}
if (!IsFastInitialized)
{
FastInitializeProperties();
}
var model = _commandItemModel.Unsafe;
if (model == null)
{
return;
}
Command.InitializeProperties();
var listIcon = model.Icon;
if (listIcon != null)
{
_listItemIcon = new(listIcon);
_listItemIcon.InitializeProperties();
}
// TODO: Do these need to go into FastInit?
model.PropChanged += Model_PropChanged;
Command.PropertyChanged += Command_PropertyChanged;
UpdateProperty(nameof(Name));
UpdateProperty(nameof(Title));
UpdateProperty(nameof(Subtitle));
UpdateProperty(nameof(Icon));
// Load-bearing: if you don't raise a IsInitialized here, then
// TopLevelViewModel will never know what the command's ID is, so it
// will never be able to load Hotkeys & aliases
UpdateProperty(nameof(IsInitialized));
Initialized |= InitializedState.Initialized;
}
public void SlowInitializeProperties()
{
if (IsSelectedInitialized)
{
return;
}
if (!IsInitialized)
{
InitializeProperties();
}
var model = _commandItemModel.Unsafe;
if (model == null)
{
return;
}
var more = model.MoreCommands;
if (more != null)
{
MoreCommands = more
.Select(item =>
{
if (item is ICommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
}
else
{
return new SeparatorContextItemViewModel() as IContextItemViewModel;
}
})
.ToList();
}
// Here, we're already theoretically in the async context, so we can
// use Initialize straight up
MoreCommands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.SlowInitializeProperties();
});
if (!string.IsNullOrEmpty(model.Command?.Name))
{
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
{
_itemTitle = Name,
Subtitle = Subtitle,
Command = Command,
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
};
// Only set the icon on the context item for us if our command didn't
// have its own icon
if (!Command.HasIcon)
{
_defaultCommandContextItem._listItemIcon = _listItemIcon;
}
}
Initialized |= InitializedState.SelectionInitialized;
UpdateProperty(nameof(MoreCommands));
UpdateProperty(nameof(AllCommands));
UpdateProperty(nameof(IsSelectedInitialized));
}
public bool SafeFastInit()
{
try
{
FastInitializeProperties();
return true;
}
catch (Exception)
{
Command = new(null, PageContext);
_itemTitle = "Error";
Subtitle = "Item failed to load";
MoreCommands = [];
_listItemIcon = _errorIcon;
Initialized |= InitializedState.Error;
}
return false;
}
public bool SafeSlowInit()
{
try
{
SlowInitializeProperties();
return true;
}
catch (Exception)
{
Initialized |= InitializedState.Error;
}
return false;
}
public bool SafeInitializeProperties()
{
try
{
InitializeProperties();
return true;
}
catch (Exception)
{
Command = new(null, PageContext);
_itemTitle = "Error";
Subtitle = "Item failed to load";
MoreCommands = [];
_listItemIcon = _errorIcon;
Initialized |= InitializedState.Error;
}
return false;
}
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
FetchProperty(args.PropertyName);
}
catch (Exception ex)
{
ShowException(ex, _commandItemModel?.Unsafe?.Title);
}
}
protected virtual void FetchProperty(string propertyName)
{
var model = this._commandItemModel.Unsafe;
if (model == null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(Command):
if (Command != null)
{
Command.PropertyChanged -= Command_PropertyChanged;
}
Command = new(model.Command, PageContext);
Command.InitializeProperties();
UpdateProperty(nameof(Name));
UpdateProperty(nameof(Title));
UpdateProperty(nameof(Icon));
break;
case nameof(Title):
_itemTitle = model.Title;
break;
case nameof(Subtitle):
this.Subtitle = model.Subtitle;
break;
case nameof(Icon):
_listItemIcon = new(model.Icon);
_listItemIcon.InitializeProperties();
break;
case nameof(model.MoreCommands):
var more = model.MoreCommands;
if (more != null)
{
var newContextMenu = more
.Select(item =>
{
if (item is CommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
}
else
{
return new SeparatorContextItemViewModel() as IContextItemViewModel;
}
})
.ToList();
lock (MoreCommands)
{
ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu);
}
newContextMenu
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
}
else
{
lock (MoreCommands)
{
MoreCommands.Clear();
}
}
UpdateProperty(nameof(SecondaryCommand));
UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasMoreCommands));
break;
}
UpdateProperty(propertyName);
}
private void Command_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var propertyName = e.PropertyName;
switch (propertyName)
{
case nameof(Command.Name):
UpdateProperty(nameof(Title));
UpdateProperty(nameof(Name));
break;
case nameof(Command.Icon):
UpdateProperty(nameof(Icon));
break;
}
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
lock (MoreCommands)
{
MoreCommands.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(c => c.SafeCleanup());
MoreCommands.Clear();
}
// _listItemIcon.SafeCleanup();
_listItemIcon = new(null); // necessary?
_defaultCommandContextItem?.SafeCleanup();
_defaultCommandContextItem = null;
Command.PropertyChanged -= Command_PropertyChanged;
Command.SafeCleanup();
var model = _commandItemModel.Unsafe;
if (model != null)
{
model.PropChanged -= Model_PropChanged;
}
}
public override void SafeCleanup()
{
base.SafeCleanup();
Initialized |= InitializedState.CleanedUp;
}
}
[Flags]
internal enum InitializedState
{
Uninitialized = 0,
FastInitialized = 1,
Initialized = 2,
SelectionInitialized = 4,
Error = 8,
CleanedUp = 16,
}

View File

@@ -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.Core.ViewModels;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandPaletteContentPageViewModel : ContentPageViewModel
{
public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host)
: base(model, scheduler, host)
{
}
public override ContentViewModel? ViewModelFromContent(IContent content, WeakReference<IPageContext> context)
{
ContentViewModel? viewModel = content switch
{
IFormContent form => new ContentFormViewModel(form, context),
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
ITreeContent tree => new ContentTreeViewModel(tree, context),
_ => null,
};
return viewModel;
}
}

View File

@@ -1,35 +1,19 @@
// Copyright (c) Microsoft Corporation
// 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 ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class CommandPaletteHost : IExtensionHost
public sealed partial class CommandPaletteHost : AppExtensionHost, 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 static ulong _hostingHwnd;
public ulong HostingHwnd => _hostingHwnd;
public string LanguageOverride => string.Empty;
public static ObservableCollection<LogMessageViewModel> LogMessages { get; } = [];
public ObservableCollection<StatusMessageViewModel> StatusMessages { get; } = [];
public IExtensionWrapper? Extension { get; }
private readonly ICommandProvider? _builtInProvider;
@@ -48,145 +32,8 @@ public sealed partial class CommandPaletteHost : IExtensionHost
_builtInProvider = builtInProvider;
}
public IAsyncAction ShowStatus(IStatusMessage? message, StatusContext context)
public override string? GetExtensionDisplayName()
{
if (message == null)
{
return Task.CompletedTask.AsAsyncAction();
}
Debug.WriteLine(message.Message);
_ = Task.Run(() =>
{
ProcessStatusMessage(message, context);
});
return Task.CompletedTask.AsAsyncAction();
}
public IAsyncAction HideStatus(IStatusMessage? message)
{
if (message == null)
{
return Task.CompletedTask.AsAsyncAction();
}
_ = Task.Run(() =>
{
ProcessHideStatusMessage(message);
});
return Task.CompletedTask.AsAsyncAction();
}
public IAsyncAction LogMessage(ILogMessage? message)
{
if (message == null)
{
return Task.CompletedTask.AsAsyncAction();
}
Logger.LogDebug(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 (Extension != null)
{
vm.ExtensionPfn = Extension.PackageFamilyName;
}
Task.Factory.StartNew(
() =>
{
LogMessages.Add(vm);
},
CancellationToken.None,
TaskCreationOptions.None,
_globalLogPageContext.Scheduler);
}
public void ProcessStatusMessage(IStatusMessage message, StatusContext context)
{
// If this message is already in the list of messages, just bring it to the top
var oldVm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault();
if (oldVm != null)
{
Task.Factory.StartNew(
() =>
{
StatusMessages.Remove(oldVm);
StatusMessages.Add(oldVm);
},
CancellationToken.None,
TaskCreationOptions.None,
_globalLogPageContext.Scheduler);
return;
}
var vm = new StatusMessageViewModel(message, new(_globalLogPageContext));
vm.SafeInitializePropertiesSynchronous();
if (Extension != null)
{
vm.ExtensionPfn = Extension.PackageFamilyName;
}
Task.Factory.StartNew(
() =>
{
StatusMessages.Add(vm);
},
CancellationToken.None,
TaskCreationOptions.None,
_globalLogPageContext.Scheduler);
}
public void ProcessHideStatusMessage(IStatusMessage message)
{
Task.Factory.StartNew(
() =>
{
try
{
var vm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault();
if (vm != null)
{
StatusMessages.Remove(vm);
}
}
catch
{
}
},
CancellationToken.None,
TaskCreationOptions.None,
_globalLogPageContext.Scheduler);
}
public static void SetHostHwnd(ulong hostHwnd) => _hostingHwnd = hostHwnd;
public void DebugLog(string message)
{
#if DEBUG
this.ProcessLogMessage(new LogMessage(message));
#endif
}
public void Log(string message)
{
this.ProcessLogMessage(new LogMessage(message));
return Extension?.ExtensionDisplayName;
}
}

View File

@@ -1,26 +1,28 @@
// Copyright (c) Microsoft Corporation
// 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.Core.ViewModels;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public class PageViewModelFactory : IPageViewModelFactoryService
public class CommandPalettePageViewModelFactory
: IPageViewModelFactoryService
{
private readonly TaskScheduler _scheduler;
public PageViewModelFactory(TaskScheduler scheduler)
public CommandPalettePageViewModelFactory(TaskScheduler scheduler)
{
_scheduler = scheduler;
}
public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, CommandPaletteHost host)
public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host)
{
return page switch
{
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested },
IContentPage contentPage => new ContentPageViewModel(contentPage, _scheduler, host),
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host),
_ => null,
};
}

View File

@@ -4,7 +4,8 @@
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.DependencyInjection;

View File

@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
{

View File

@@ -1,133 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandViewModel : ExtensionObjectViewModel
{
public ExtensionObject<ICommand> Model { get; private set; } = new(null);
protected bool IsInitialized { get; private set; }
protected bool IsFastInitialized { get; private set; }
public bool HasIcon => Icon.IsSet;
// 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
// [ObservableProperty]s, because PropChanged comes in off the UI thread,
// and ObservableProperty is not smart enough to raise the PropertyChanged
// on the UI thread.
public string Id { get; private set; } = string.Empty;
public string Name { get; private set; } = string.Empty;
public IconInfoViewModel Icon { get; private set; }
public CommandViewModel(ICommand? command, WeakReference<IPageContext> pageContext)
: base(pageContext)
{
Model = new(command);
Icon = new(null);
}
public void FastInitializeProperties()
{
if (IsFastInitialized)
{
return;
}
var model = Model.Unsafe;
if (model == null)
{
return;
}
Id = model.Id ?? string.Empty;
Name = model.Name ?? string.Empty;
IsFastInitialized = true;
}
public override void InitializeProperties()
{
if (IsInitialized)
{
return;
}
if (!IsFastInitialized)
{
FastInitializeProperties();
}
var model = Model.Unsafe;
if (model == null)
{
return;
}
var ico = model.Icon;
if (ico != null)
{
Icon = new(ico);
Icon.InitializeProperties();
UpdateProperty(nameof(Icon));
}
model.PropChanged += Model_PropChanged;
}
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
FetchProperty(args.PropertyName);
}
catch (Exception ex)
{
ShowException(ex, Name);
}
}
protected void FetchProperty(string propertyName)
{
var model = Model.Unsafe;
if (model == null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(Name):
Name = model.Name;
break;
case nameof(Icon):
var iconInfo = model.Icon;
Icon = new(iconInfo);
Icon.InitializeProperties();
break;
}
UpdateProperty(propertyName);
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
Icon = new(null); // necessary?
var model = Model.Unsafe;
if (model != null)
{
model.PropChanged -= Model_PropChanged;
}
}
}

View File

@@ -2,7 +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 Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;

View File

@@ -2,15 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.CmdPal.Common;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Specialized;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -10,7 +11,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Commands;
public partial class LogMessagesPage : ListPage
{
private readonly List<IListItem> _listItems = new();
private readonly List<IListItem> _listItems = [];
public LogMessagesPage()
{
@@ -31,7 +32,6 @@ public partial class LogMessagesPage : ListPage
var li = new ListItem(new NoOpCommand())
{
Title = logMessageViewModel.Message,
Subtitle = logMessageViewModel.ExtensionPfn,
};
_listItems.Insert(0, li);
}

View File

@@ -6,8 +6,8 @@ using System.Collections.Immutable;
using System.Collections.Specialized;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Ext.Apps;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.DependencyInjection;

View File

@@ -6,7 +6,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;

View File

@@ -1,44 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ConfirmResultViewModel(IConfirmationArgs _args, WeakReference<IPageContext> context) :
ExtensionObjectViewModel(context)
{
public ExtensionObject<IConfirmationArgs> Model { get; } = new(_args);
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
public string Title { get; private set; } = string.Empty;
public string Description { get; private set; } = string.Empty;
public bool IsPrimaryCommandCritical { get; private set; }
public CommandViewModel PrimaryCommand { get; private set; } = new(null, context);
public override void InitializeProperties()
{
var model = Model.Unsafe;
if (model == null)
{
return;
}
Title = model.Title;
Description = model.Description;
IsPrimaryCommandCritical = model.IsPrimaryCommandCritical;
PrimaryCommand = new(model.PrimaryCommand, PageContext);
PrimaryCommand.InitializeProperties();
UpdateProperty(nameof(Title));
UpdateProperty(nameof(Description));
UpdateProperty(nameof(IsPrimaryCommandCritical));
UpdateProperty(nameof(PrimaryCommand));
}
}

View File

@@ -7,8 +7,9 @@ using AdaptiveCards.ObjectModel.WinUI3;
using AdaptiveCards.Templating;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Windows.Data.Json;

View File

@@ -2,7 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;

View File

@@ -1,297 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
{
private readonly ExtensionObject<IContentPage> _model;
[ObservableProperty]
public partial ObservableCollection<ContentViewModel> Content { get; set; } = [];
public List<IContextItemViewModel> Commands { get; private set; } = [];
public bool HasCommands => ActualCommands.Count > 0;
public DetailsViewModel? Details { get; private set; }
[MemberNotNullWhen(true, nameof(Details))]
public bool HasDetails => Details != null;
/////// ICommandBarContext ///////
public IEnumerable<IContextItemViewModel> MoreCommands => Commands.Skip(1);
private List<CommandContextItemViewModel> ActualCommands => Commands.OfType<CommandContextItemViewModel>().ToList();
public bool HasMoreCommands => ActualCommands.Count > 1;
public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
public CommandItemViewModel? PrimaryCommand => HasCommands ? ActualCommands[0] : null;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[1] : null;
public List<IContextItemViewModel> AllCommands => Commands;
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, CommandPaletteHost host)
: base(model, scheduler, host)
{
_model = new(model);
}
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchContent();
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
private void FetchContent()
{
List<ContentViewModel> newContent = [];
try
{
var newItems = _model.Unsafe!.GetContent();
foreach (var item in newItems)
{
var viewModel = ViewModelFromContent(item, PageContext);
if (viewModel != null)
{
viewModel.InitializeProperties();
newContent.Add(viewModel);
}
}
}
catch (Exception ex)
{
ShowException(ex, _model?.Unsafe?.Name);
throw;
}
var oneContent = newContent.Count == 1;
newContent.ForEach(c => c.OnlyControlOnPage = oneContent);
// Now, back to a UI thread to update the observable collection
DoOnUiThread(
() =>
{
ListHelpers.InPlaceUpdateList(Content, newContent);
});
}
public static ContentViewModel? ViewModelFromContent(IContent content, WeakReference<IPageContext> context)
{
ContentViewModel? viewModel = content switch
{
IFormContent form => new ContentFormViewModel(form, context),
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
ITreeContent tree => new ContentTreeViewModel(tree, context),
_ => null,
};
return viewModel;
}
public override void InitializeProperties()
{
base.InitializeProperties();
var model = _model.Unsafe;
if (model == null)
{
return; // throw?
}
Commands = model.Commands
.ToList()
.Select(item =>
{
if (item is CommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
}
else
{
return new SeparatorContextItemViewModel() as IContextItemViewModel;
}
})
.ToList();
Commands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
var extensionDetails = model.Details;
if (extensionDetails != null)
{
Details = new(extensionDetails, PageContext);
Details.InitializeProperties();
}
UpdateDetails();
FetchContent();
model.ItemsChanged += Model_ItemsChanged;
DoOnUiThread(
() =>
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(this));
});
}
protected override void FetchProperty(string propertyName)
{
base.FetchProperty(propertyName);
var model = this._model.Unsafe;
if (model == null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(Commands):
var more = model.Commands;
if (more != null)
{
var newContextMenu = more
.ToList()
.Select(item =>
{
if (item is CommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
}
else
{
return new SeparatorContextItemViewModel() as IContextItemViewModel;
}
})
.ToList();
lock (Commands)
{
ListHelpers.InPlaceUpdateList(Commands, newContextMenu);
}
Commands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.SlowInitializeProperties();
});
}
else
{
Commands.Clear();
}
UpdateProperty(nameof(PrimaryCommand));
UpdateProperty(nameof(SecondaryCommand));
UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasCommands));
UpdateProperty(nameof(HasMoreCommands));
UpdateProperty(nameof(AllCommands));
DoOnUiThread(
() =>
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(this));
});
break;
case nameof(Details):
var extensionDetails = model.Details;
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
UpdateDetails();
break;
}
UpdateProperty(propertyName);
}
private void UpdateDetails()
{
UpdateProperty(nameof(Details));
UpdateProperty(nameof(HasDetails));
DoOnUiThread(
() =>
{
if (HasDetails)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(Details));
}
else
{
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
}
});
}
// InvokeItemCommand is what this will be in Xaml due to source generator
// this comes in on Enter keypresses in the SearchBox
[RelayCommand]
private void InvokePrimaryCommand(ContentPageViewModel page)
{
if (PrimaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
}
}
// this comes in on Ctrl+Enter keypresses in the SearchBox
[RelayCommand]
private void InvokeSecondaryCommand(ContentPageViewModel page)
{
if (SecondaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
}
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
Details?.SafeCleanup();
Commands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(item => item.SafeCleanup());
Commands.Clear();
foreach (var item in Content)
{
item.SafeCleanup();
}
Content.Clear();
var model = _model.Unsafe;
if (model != null)
{
model.ItemsChanged -= Model_ItemsChanged;
}
}
}

View File

@@ -3,7 +3,8 @@
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -37,7 +38,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
var root = model.RootContent;
if (root != null)
{
RootContent = ContentPageViewModel.ViewModelFromContent(root, PageContext);
RootContent = ViewModelFromContent(root, PageContext);
RootContent?.InitializeProperties();
UpdateProperty(nameof(RootContent));
UpdateProperty(nameof(Root));
@@ -48,6 +49,20 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
model.ItemsChanged += Model_ItemsChanged;
}
// Theoretically, we should unify this with the one in CommandPalettePageViewModelFactory
// and maybe just have a ContentViewModelFactory or something
public ContentViewModel? ViewModelFromContent(IContent content, WeakReference<IPageContext> context)
{
ContentViewModel? viewModel = content switch
{
IFormContent form => new ContentFormViewModel(form, context),
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
ITreeContent tree => new ContentTreeViewModel(tree, context),
_ => null,
};
return viewModel;
}
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchContent();
@@ -78,7 +93,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
var root = model.RootContent;
if (root != null)
{
RootContent = ContentPageViewModel.ViewModelFromContent(root, PageContext);
RootContent = ViewModelFromContent(root, PageContext);
}
else
{
@@ -103,7 +118,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
foreach (var item in newItems)
{
var viewModel = ContentPageViewModel.ViewModelFromContent(item, PageContext);
var viewModel = ViewModelFromContent(item, PageContext);
if (viewModel != null)
{
viewModel.InitializeProperties();

View File

@@ -1,11 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels;
public abstract partial class ContentViewModel(WeakReference<IPageContext> context) :
ExtensionObjectViewModel(context)
{
public bool OnlyControlOnPage { get; internal set; }
}

View File

@@ -1,267 +0,0 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Diagnostics.Utilities;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContextMenuViewModel : ObservableObject,
IRecipient<UpdateCommandBarMessage>,
IRecipient<OpenContextMenuMessage>
{
public ICommandBarContext? SelectedItem
{
get => field;
set
{
if (field != null)
{
field.PropertyChanged -= SelectedItemPropertyChanged;
}
field = value;
SetSelectedItem(value);
OnPropertyChanged(nameof(SelectedItem));
}
}
[ObservableProperty]
private partial ObservableCollection<List<IContextItemViewModel>> ContextMenuStack { get; set; } = [];
private List<IContextItemViewModel>? CurrentContextMenu => ContextMenuStack.LastOrDefault();
[ObservableProperty]
public partial ObservableCollection<IContextItemViewModel> FilteredItems { get; set; } = [];
[ObservableProperty]
public partial bool FilterOnTop { get; set; } = false;
private string _lastSearchText = string.Empty;
public ContextMenuViewModel()
{
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
}
public void Receive(UpdateCommandBarMessage message)
{
SelectedItem = message.ViewModel;
}
public void Receive(OpenContextMenuMessage message)
{
FilterOnTop = message.ContextMenuFilterLocation == ContextMenuFilterLocation.Top;
ResetContextMenu();
OnPropertyChanging(nameof(FilterOnTop));
OnPropertyChanged(nameof(FilterOnTop));
}
private void SetSelectedItem(ICommandBarContext? value)
{
if (value != null)
{
value.PropertyChanged += SelectedItemPropertyChanged;
}
else
{
if (SelectedItem != null)
{
SelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
}
}
UpdateContextItems();
}
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(SelectedItem.HasMoreCommands):
UpdateContextItems();
break;
}
}
public void UpdateContextItems()
{
if (SelectedItem != null)
{
if (SelectedItem.MoreCommands.Count() > 1)
{
ContextMenuStack.Clear();
PushContextStack(SelectedItem.AllCommands);
}
}
}
public void SetSearchText(string searchText)
{
if (searchText == _lastSearchText)
{
return;
}
if (SelectedItem == null)
{
return;
}
_lastSearchText = searchText;
if (CurrentContextMenu == null)
{
ListHelpers.InPlaceUpdateList(FilteredItems, []);
return;
}
if (string.IsNullOrEmpty(searchText))
{
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu]);
return;
}
var commands = CurrentContextMenu
.OfType<CommandContextItemViewModel>()
.Where(c => c.ShouldBeVisible);
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
}
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
{
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
{
return 1;
}
if (string.IsNullOrEmpty(item.Title))
{
return 0;
}
var nameMatch = StringMatcher.FuzzySearch(query, item.Title);
var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle);
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
}
/// <summary>
/// Generates a mapping of key -> command item for this particular item's
/// MoreCommands. (This won't include the primary Command, but it will
/// include the secondary one). This map can be used to quickly check if a
/// shortcut key was pressed
/// </summary>
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
/// that have a shortcut key set.</returns>
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
if (CurrentContextMenu == null)
{
return [];
}
return CurrentContextMenu
.OfType<CommandContextItemViewModel>()
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
public ContextKeybindingResult? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var keybindings = Keybindings();
if (keybindings != null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (keybindings.TryGetValue(pressedKeyChord, out var item))
{
return InvokeCommand(item);
}
}
return null;
}
public bool CanPopContextStack()
{
return ContextMenuStack.Count > 1;
}
public void PopContextStack()
{
if (ContextMenuStack.Count > 1)
{
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
OnPropertyChanging(nameof(CurrentContextMenu));
OnPropertyChanged(nameof(CurrentContextMenu));
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
}
private void PushContextStack(IEnumerable<IContextItemViewModel> commands)
{
ContextMenuStack.Add(commands.ToList());
OnPropertyChanging(nameof(CurrentContextMenu));
OnPropertyChanged(nameof(CurrentContextMenu));
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
}
private void ResetContextMenu()
{
while (ContextMenuStack.Count > 1)
{
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
OnPropertyChanging(nameof(CurrentContextMenu));
OnPropertyChanged(nameof(CurrentContextMenu));
if (CurrentContextMenu != null)
{
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
}
}
public ContextKeybindingResult InvokeCommand(CommandItemViewModel? command)
{
if (command == null)
{
return ContextKeybindingResult.Unhandled;
}
if (command.HasMoreCommands)
{
// Display the commands child commands
PushContextStack(command.AllCommands);
OnPropertyChanging(nameof(FilteredItems));
OnPropertyChanged(nameof(FilteredItems));
return ContextKeybindingResult.KeepOpen;
}
else
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
UpdateContextItems();
return ContextKeybindingResult.Hide;
}
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class DetailsCommandsViewModel(
IDetailsElement _detailsElement,
WeakReference<IPageContext> context) : DetailsElementViewModel(_detailsElement, context)
{
public List<CommandViewModel> Commands { get; private set; } = [];
public bool HasCommands => Commands.Count > 0;
private readonly ExtensionObject<IDetailsCommands> _dataModel =
new(_detailsElement.Data as IDetailsCommands);
public override void InitializeProperties()
{
base.InitializeProperties();
var model = _dataModel.Unsafe;
if (model == null)
{
return;
}
Commands = model
.Commands?
.Select(c =>
{
var vm = new CommandViewModel(c, PageContext);
vm.InitializeProperties();
return vm;
})
.ToList() ?? [];
UpdateProperty(nameof(HasCommands));
UpdateProperty(nameof(Commands));
}
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public abstract partial class DetailsDataViewModel(IPageContext context) : ExtensionObjectViewModel(context)
{
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public abstract partial class DetailsElementViewModel(IDetailsElement _detailsElement, WeakReference<IPageContext> context) : ExtensionObjectViewModel(context)
{
private readonly ExtensionObject<IDetailsElement> _model = new(_detailsElement);
public string Key { get; private set; } = string.Empty;
public override void InitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
{
return;
}
Key = model.Key ?? string.Empty;
UpdateProperty(nameof(Key));
}
}

View File

@@ -1,46 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class DetailsLinkViewModel(
IDetailsElement _detailsElement,
WeakReference<IPageContext> context) : DetailsElementViewModel(_detailsElement, context)
{
private readonly ExtensionObject<IDetailsLink> _dataModel =
new(_detailsElement.Data as IDetailsLink);
public string Text { get; private set; } = string.Empty;
public Uri? Link { get; private set; }
public bool IsLink => Link != null;
public bool IsText => !IsLink;
public override void InitializeProperties()
{
base.InitializeProperties();
var model = _dataModel.Unsafe;
if (model == null)
{
return;
}
Text = model.Text ?? string.Empty;
Link = model.Link;
if (string.IsNullOrEmpty(Text) && Link != null)
{
Text = Link.ToString();
}
UpdateProperty(nameof(Text));
UpdateProperty(nameof(Link));
UpdateProperty(nameof(IsLink));
UpdateProperty(nameof(IsText));
}
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class DetailsSeparatorViewModel(
IDetailsElement _detailsElement,
WeakReference<IPageContext> context) : DetailsElementViewModel(_detailsElement, context)
{
private readonly ExtensionObject<IDetailsSeparator> _dataModel =
new(_detailsElement.Data as IDetailsSeparator);
public override void InitializeProperties()
{
base.InitializeProperties();
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class DetailsTagsViewModel(
IDetailsElement _detailsElement,
WeakReference<IPageContext> context) : DetailsElementViewModel(_detailsElement, context)
{
public List<TagViewModel> Tags { get; private set; } = [];
public bool HasTags => Tags.Count > 0;
private readonly ExtensionObject<IDetailsTags> _dataModel =
new(_detailsElement.Data as IDetailsTags);
public override void InitializeProperties()
{
base.InitializeProperties();
var model = _dataModel.Unsafe;
if (model == null)
{
return;
}
Tags = model
.Tags?
.Select(t =>
{
var vm = new TagViewModel(t, PageContext);
vm.InitializeProperties();
return vm;
})
.ToList() ?? [];
UpdateProperty(nameof(HasTags));
UpdateProperty(nameof(Tags));
}
}

View File

@@ -1,64 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class DetailsViewModel(IDetails _details, WeakReference<IPageContext> context) : ExtensionObjectViewModel(context)
{
private readonly ExtensionObject<IDetails> _detailsModel = new(_details);
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
public IconInfoViewModel HeroImage { get; private set; } = new(null);
public string Title { get; private set; } = string.Empty;
public string Body { get; private set; } = string.Empty;
// Metadata is an array of IDetailsElement,
// where IDetailsElement = {IDetailsTags, IDetailsLink, IDetailsSeparator}
public List<DetailsElementViewModel> Metadata { get; private set; } = [];
public override void InitializeProperties()
{
var model = _detailsModel.Unsafe;
if (model == null)
{
return;
}
Title = model.Title ?? string.Empty;
Body = model.Body ?? string.Empty;
HeroImage = new(model.HeroImage);
HeroImage.InitializeProperties();
UpdateProperty(nameof(Title));
UpdateProperty(nameof(Body));
UpdateProperty(nameof(HeroImage));
var meta = model.Metadata;
if (meta != null)
{
foreach (var element in meta)
{
DetailsElementViewModel? vm = element.Data switch
{
IDetailsSeparator => new DetailsSeparatorViewModel(element, this.PageContext),
IDetailsLink => new DetailsLinkViewModel(element, this.PageContext),
IDetailsCommands => new DetailsCommandsViewModel(element, this.PageContext),
IDetailsTags => new DetailsTagsViewModel(element, this.PageContext),
_ => null,
};
if (vm != null)
{
vm.InitializeProperties();
Metadata.Add(vm);
}
}
}
}
}

View File

@@ -1,90 +0,0 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
namespace Microsoft.CmdPal.UI.ViewModels;
public abstract partial class ExtensionObjectViewModel : ObservableObject
{
public WeakReference<IPageContext> PageContext { get; set; }
public ExtensionObjectViewModel(IPageContext? context)
{
var realContext = context ?? (this is IPageContext c ? c : throw new ArgumentException("You need to pass in an IErrorContext"));
PageContext = new(realContext);
}
public ExtensionObjectViewModel(WeakReference<IPageContext> context)
{
PageContext = context;
}
public async virtual Task InitializePropertiesAsync()
{
var t = new Task(() =>
{
SafeInitializePropertiesSynchronous();
});
t.Start();
await t;
}
public void SafeInitializePropertiesSynchronous()
{
try
{
InitializeProperties();
}
catch (Exception ex)
{
ShowException(ex);
}
}
public abstract void InitializeProperties();
protected void UpdateProperty(string propertyName)
{
DoOnUiThread(() => OnPropertyChanged(propertyName));
}
protected void ShowException(Exception ex, string? extensionHint = null)
{
if (PageContext.TryGetTarget(out var pageContext))
{
pageContext.ShowException(ex, extensionHint);
}
}
protected void DoOnUiThread(Action action)
{
if (PageContext.TryGetTarget(out var pageContext))
{
Task.Factory.StartNew(
action,
CancellationToken.None,
TaskCreationOptions.None,
pageContext.Scheduler);
}
}
protected virtual void UnsafeCleanup()
{
// base doesn't do anything, but sub-classes should override this.
}
public virtual void SafeCleanup()
{
try
{
UnsafeCleanup();
}
catch (Exception ex)
{
Logger.LogDebug(ex.ToString());
}
}
}

View File

@@ -1,19 +0,0 @@
// 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();
}
}

View File

@@ -1,15 +0,0 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.UI.ViewModels;
public interface IContextItemViewModel
{
}

View File

@@ -1,47 +0,0 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class IconDataViewModel : ObservableObject, IIconData
{
private readonly ExtensionObject<IIconData> _model = new(null);
// If the extension previously gave us a Data, then died, the data will
// throw if we actually try to read it, but the pointer itself won't be
// null, so this is relatively safe.
public bool HasIcon => !string.IsNullOrEmpty(Icon) || Data.Unsafe != null;
// Locally cached properties from IIconData.
public string Icon { get; private set; } = string.Empty;
// Streams are not trivially copy-able, so we can't copy the data locally
// first. Hence why we're sticking this into an ExtensionObject
public ExtensionObject<IRandomAccessStreamReference> Data { get; private set; } = new(null);
IRandomAccessStreamReference? IIconData.Data => Data.Unsafe;
public IconDataViewModel(IIconData? icon)
{
_model = new(icon);
}
// Unsafe, needs to be called on BG thread
public void InitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
{
return;
}
Icon = model.Icon;
Data = new(model.Data);
}
}

View File

@@ -1,57 +0,0 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class IconInfoViewModel : ObservableObject, IIconInfo
{
private readonly ExtensionObject<IIconInfo> _model = new(null);
// 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
// [ObservableProperty]s, because PropChanged comes in off the UI thread,
// and ObservableProperty is not smart enough to raise the PropertyChanged
// on the UI thread.
public IconDataViewModel Light { get; private set; }
public IconDataViewModel Dark { get; private set; }
public IconDataViewModel IconForTheme(bool light) => Light = light ? Light : Dark;
public bool HasIcon(bool light) => IconForTheme(light).HasIcon;
public bool IsSet => _model.Unsafe != null;
IIconData? IIconInfo.Dark => Dark;
IIconData? IIconInfo.Light => Light;
public IconInfoViewModel(IIconInfo? icon)
{
_model = new(icon);
Light = new(null);
Dark = new(null);
}
// Unsafe, needs to be called on BG thread
public void InitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
{
return;
}
Light = new(model.Light);
Light.InitializeProperties();
Dark = new(model.Dark);
Dark.InitializeProperties();
}
}

View File

@@ -1,146 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
: CommandItemViewModel(new(model), context)
{
public new ExtensionObject<IListItem> Model { get; } = new(model);
public List<TagViewModel>? Tags { get; set; }
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
public bool HasTags => (Tags?.Count ?? 0) > 0;
public string TextToSuggest { get; private set; } = string.Empty;
public string Section { get; private set; } = string.Empty;
public DetailsViewModel? Details { get; private set; }
[MemberNotNullWhen(true, nameof(Details))]
public bool HasDetails => Details != null;
public override void InitializeProperties()
{
if (IsInitialized)
{
return;
}
// This sets IsInitialized = true
base.InitializeProperties();
var li = Model.Unsafe;
if (li == null)
{
return; // throw?
}
UpdateTags(li.Tags);
TextToSuggest = li.TextToSuggest;
Section = li.Section ?? string.Empty;
var extensionDetails = li.Details;
if (extensionDetails != null)
{
Details = new(extensionDetails, PageContext);
Details.InitializeProperties();
UpdateProperty(nameof(Details));
UpdateProperty(nameof(HasDetails));
}
UpdateProperty(nameof(TextToSuggest));
UpdateProperty(nameof(Section));
}
protected override void FetchProperty(string propertyName)
{
base.FetchProperty(propertyName);
var model = this.Model.Unsafe;
if (model == null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(Tags):
UpdateTags(model.Tags);
break;
case nameof(TextToSuggest):
this.TextToSuggest = model.TextToSuggest ?? string.Empty;
break;
case nameof(Section):
this.Section = model.Section ?? string.Empty;
break;
case nameof(Details):
var extensionDetails = model.Details;
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
Details?.InitializeProperties();
UpdateProperty(nameof(Details));
UpdateProperty(nameof(HasDetails));
break;
}
UpdateProperty(propertyName);
}
// TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes?
// TODO: Do we want to save off the score here so we can sort by it in our ListViewModel?
public bool MatchesFilter(string filter) => StringMatcher.FuzzySearch(filter, Title).Success || StringMatcher.FuzzySearch(filter, Subtitle).Success;
public override string ToString() => $"{Name} ListItemViewModel";
public override bool Equals(object? obj) => obj is ListItemViewModel vm && vm.Model.Equals(this.Model);
public override int GetHashCode() => Model.GetHashCode();
private void UpdateTags(ITag[]? newTagsFromModel)
{
var newTags = newTagsFromModel?.Select(t =>
{
var vm = new TagViewModel(t, PageContext);
vm.InitializeProperties();
return vm;
})
.ToList() ?? [];
DoOnUiThread(
() =>
{
// Tags being an ObservableCollection instead of a List lead to
// many COM exception issues.
Tags = new(newTags);
UpdateProperty(nameof(Tags));
UpdateProperty(nameof(HasTags));
});
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
// Tags don't have event handlers or anything to cleanup
Tags?.ForEach(t => t.SafeCleanup());
Details?.SafeCleanup();
var model = Model.Unsafe;
if (model != null)
{
// We don't need to revoke the PropChanged event handler here,
// because we are just overriding CommandItem's FetchProperty and
// piggy-backing off their PropChanged
}
}
}

View File

@@ -1,576 +0,0 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ListViewModel : PageViewModel, IDisposable
{
// private readonly HashSet<ListItemViewModel> _itemCache = [];
// TODO: Do we want a base "ItemsPageViewModel" for anything that's going to have items?
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
[ObservableProperty]
public partial ObservableCollection<ListItemViewModel> FilteredItems { get; set; } = [];
private ObservableCollection<ListItemViewModel> Items { get; set; } = [];
private readonly ExtensionObject<IListPage> _model;
private readonly Lock _listLock = new();
private bool _isLoading;
private bool _isFetching;
public event TypedEventHandler<ListViewModel, object>? ItemsUpdated;
public bool ShowEmptyContent =>
IsInitialized &&
FilteredItems.Count == 0 &&
(!_isFetching) &&
IsLoading == false;
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
public bool ShowDetails { get; private set; }
private string _modelPlaceholderText = string.Empty;
public override string PlaceholderText => _modelPlaceholderText;
public string SearchText { get; private set; } = string.Empty;
public string InitialSearchText { get; private set; } = string.Empty;
public CommandItemViewModel EmptyContent { get; private set; }
private bool _isDynamic;
private Task? _initializeItemsTask;
private CancellationTokenSource? _cancellationTokenSource;
private ListItemViewModel? _lastSelectedItem;
public override bool IsInitialized
{
get => base.IsInitialized; protected set
{
base.IsInitialized = value;
UpdateEmptyContent();
}
}
public ListViewModel(IListPage model, TaskScheduler scheduler, CommandPaletteHost host)
: base(model, scheduler, host)
{
_model = new(model);
EmptyContent = new(new(null), PageContext);
}
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchItems();
protected override void OnFilterUpdated(string filter)
{
//// TODO: Just temp testing, need to think about where we want to filter, as AdvancedCollectionView in View could be done, but then grouping need CollectionViewSource, maybe we do grouping in view
//// and manage filtering below, but we should be smarter about this and understand caching and other requirements...
//// Investigate if we re-use src\modules\cmdpal\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\ListHelpers.cs InPlaceUpdateList and FilterList?
// Dynamic pages will handler their own filtering. They will tell us if
// something needs to change, by raising ItemsChanged.
if (_isDynamic)
{
// We're getting called on the UI thread.
// Hop off to a BG thread to update the extension.
_ = Task.Run(() =>
{
try
{
if (_model.Unsafe is IDynamicListPage dynamic)
{
dynamic.SearchText = filter;
}
}
catch (Exception ex)
{
ShowException(ex, _model?.Unsafe?.Name);
}
});
}
else
{
// But for all normal pages, we should run our fuzzy match on them.
lock (_listLock)
{
ApplyFilterUnderLock();
}
ItemsUpdated?.Invoke(this, EventArgs.Empty);
UpdateEmptyContent();
_isLoading = false;
}
}
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
private void FetchItems()
{
// TEMPORARY: just plop all the items into a single group
// see 9806fe5d8 for the last commit that had this with sections
_isFetching = true;
try
{
var newItems = _model.Unsafe!.GetItems();
// Collect all the items into new viewmodels
Collection<ListItemViewModel> newViewModels = [];
// TODO we can probably further optimize this by also keeping a
// HashSet of every ExtensionObject we currently have, and only
// building new viewmodels for the ones we haven't already built.
foreach (var item in newItems)
{
ListItemViewModel viewModel = new(item, new(this));
// If an item fails to load, silently ignore it.
if (viewModel.SafeFastInit())
{
newViewModels.Add(viewModel);
}
}
var firstTwenty = newViewModels.Take(20);
foreach (var item in firstTwenty)
{
item?.SafeInitializeProperties();
}
// Cancel any ongoing search
if (_cancellationTokenSource != null)
{
_cancellationTokenSource.Cancel();
}
lock (_listLock)
{
// Now that we have new ViewModels for everything from the
// extension, smartly update our list of VMs
ListHelpers.InPlaceUpdateList(Items, newViewModels);
}
// TODO: Iterate over everything in Items, and prune items from the
// cache if we don't need them anymore
}
catch (Exception ex)
{
// TODO: Move this within the for loop, so we can catch issues with individual items
// Create a special ListItemViewModel for errors and use an ItemTemplateSelector in the ListPage to display error items differently.
ShowException(ex, _model?.Unsafe?.Name);
throw;
}
finally
{
_isFetching = false;
}
_cancellationTokenSource = new CancellationTokenSource();
_initializeItemsTask = new Task(() =>
{
try
{
InitializeItemsTask(_cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
}
});
_initializeItemsTask.Start();
DoOnUiThread(
() =>
{
lock (_listLock)
{
// Now that our Items contains everything we want, it's time for us to
// re-evaluate our Filter on those items.
if (!_isDynamic)
{
// A static list? Great! Just run the filter.
ApplyFilterUnderLock();
}
else
{
// A dynamic list? Even better! Just stick everything into
// FilteredItems. The extension already did any filtering it cared about.
ListHelpers.InPlaceUpdateList(FilteredItems, Items.Where(i => !i.IsInErrorState));
}
UpdateEmptyContent();
}
ItemsUpdated?.Invoke(this, EventArgs.Empty);
_isLoading = false;
});
}
private void InitializeItemsTask(CancellationToken ct)
{
// Were we already canceled?
ct.ThrowIfCancellationRequested();
ListItemViewModel[] iterable;
lock (_listLock)
{
iterable = Items.ToArray();
}
foreach (var item in iterable)
{
ct.ThrowIfCancellationRequested();
// TODO: GH #502
// We should probably remove the item from the list if it
// entered the error state. I had issues doing that without having
// multiple threads muck with `Items` (and possibly FilteredItems!)
// at once.
item.SafeInitializeProperties();
ct.ThrowIfCancellationRequested();
}
}
/// <summary>
/// Apply our current filter text to the list of items, and update
/// FilteredItems to match the results.
/// </summary>
private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, Filter));
/// <summary>
/// Helper to generate a weighting for a given list item, based on title,
/// subtitle, etc. Largely a copy of the version in ListHelpers, but
/// operating on ViewModels instead of extension objects.
/// </summary>
private static int ScoreListItem(string query, CommandItemViewModel listItem)
{
if (string.IsNullOrEmpty(query))
{
return 1;
}
var nameMatch = StringMatcher.FuzzySearch(query, listItem.Title);
var descriptionMatch = StringMatcher.FuzzySearch(query, listItem.Subtitle);
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
}
private struct ScoredListItemViewModel
{
public int Score;
public ListItemViewModel ViewModel;
}
// Similarly stolen from ListHelpers.FilterList
public static IEnumerable<ListItemViewModel> FilterList(IEnumerable<ListItemViewModel> items, string query)
{
var scores = items
.Where(i => !i.IsInErrorState)
.Select(li => new ScoredListItemViewModel() { ViewModel = li, Score = ScoreListItem(query, li) })
.Where(score => score.Score > 0)
.OrderByDescending(score => score.Score);
return scores
.Select(score => score.ViewModel);
}
// InvokeItemCommand is what this will be in Xaml due to source generator
// This is what gets invoked when the user presses <enter>
[RelayCommand]
private void InvokeItem(ListItemViewModel? item)
{
if (item != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
}
else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(
EmptyContent.PrimaryCommand.Command.Model,
EmptyContent.PrimaryCommand.Model));
}
}
// This is what gets invoked when the user presses <ctrl+enter>
[RelayCommand]
private void InvokeSecondaryCommand(ListItemViewModel? item)
{
if (item != null)
{
if (item.SecondaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.SecondaryCommand.Command.Model, item.Model));
}
}
else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(
EmptyContent.SecondaryCommand.Command.Model,
EmptyContent.SecondaryCommand.Model));
}
}
[RelayCommand]
private void UpdateSelectedItem(ListItemViewModel? item)
{
if (_lastSelectedItem != null)
{
_lastSelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
}
if (item != null)
{
SetSelectedItem(item);
}
else
{
ClearSelectedItem();
}
}
private void SetSelectedItem(ListItemViewModel item)
{
if (!item.SafeSlowInit())
{
return;
}
// GH #322:
// For inexplicable reasons, if you try updating the command bar and
// the details on the same UI thread tick as updating the list, we'll
// explode
DoOnUiThread(
() =>
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
}
else
{
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
}
TextToSuggest = item.TextToSuggest;
});
_lastSelectedItem = item;
_lastSelectedItem.PropertyChanged += SelectedItemPropertyChanged;
}
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var item = _lastSelectedItem;
if (item == null)
{
return;
}
// already on the UI thread here
switch (e.PropertyName)
{
case nameof(item.Command):
case nameof(item.SecondaryCommand):
case nameof(item.AllCommands):
case nameof(item.Name):
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
break;
case nameof(item.Details):
if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
}
else
{
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
}
break;
case nameof(item.TextToSuggest):
TextToSuggest = item.TextToSuggest;
break;
}
}
private void ClearSelectedItem()
{
// GH #322:
// For inexplicable reasons, if you try updating the command bar and
// the details on the same UI thread tick as updating the list, we'll
// explode
DoOnUiThread(
() =>
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
TextToSuggest = string.Empty;
});
}
public override void InitializeProperties()
{
base.InitializeProperties();
var model = _model.Unsafe;
if (model == null)
{
return; // throw?
}
_isDynamic = model is IDynamicListPage;
ShowDetails = model.ShowDetails;
UpdateProperty(nameof(ShowDetails));
_modelPlaceholderText = model.PlaceholderText;
UpdateProperty(nameof(PlaceholderText));
InitialSearchText = SearchText = model.SearchText;
UpdateProperty(nameof(SearchText));
UpdateProperty(nameof(InitialSearchText));
EmptyContent = new(new(model.EmptyContent), PageContext);
EmptyContent.SlowInitializeProperties();
FetchItems();
model.ItemsChanged += Model_ItemsChanged;
}
public void LoadMoreIfNeeded()
{
var model = this._model.Unsafe;
if (model == null)
{
return;
}
if (model.HasMoreItems && !_isLoading)
{
_isLoading = true;
_ = Task.Run(() =>
{
try
{
model.LoadMore();
}
catch (Exception ex)
{
ShowException(ex, model.Name);
}
});
}
}
protected override void FetchProperty(string propertyName)
{
base.FetchProperty(propertyName);
var model = this._model.Unsafe;
if (model == null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(ShowDetails):
this.ShowDetails = model.ShowDetails;
break;
case nameof(PlaceholderText):
this._modelPlaceholderText = model.PlaceholderText;
break;
case nameof(SearchText):
this.SearchText = model.SearchText;
break;
case nameof(EmptyContent):
EmptyContent = new(new(model.EmptyContent), PageContext);
EmptyContent.SlowInitializeProperties();
break;
case nameof(IsLoading):
UpdateEmptyContent();
break;
}
UpdateProperty(propertyName);
}
private void UpdateEmptyContent()
{
UpdateProperty(nameof(ShowEmptyContent));
if (!ShowEmptyContent || EmptyContent.Model.Unsafe == null)
{
return;
}
UpdateProperty(nameof(EmptyContent));
DoOnUiThread(
() =>
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(EmptyContent));
});
}
public void Dispose()
{
GC.SuppressFinalize(this);
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
EmptyContent?.SafeCleanup();
EmptyContent = new(new(null), PageContext); // necessary?
_cancellationTokenSource?.Cancel();
lock (_listLock)
{
foreach (var item in Items)
{
item.SafeCleanup();
}
Items.Clear();
foreach (var item in FilteredItems)
{
item.SafeCleanup();
}
FilteredItems.Clear();
}
var model = _model.Unsafe;
if (model != null)
{
model.ItemsChanged -= Model_ItemsChanged;
}
}
}

View File

@@ -1,17 +0,0 @@
// 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.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class LoadingPageViewModel : PageViewModel
{
public LoadingPageViewModel(IPage? model, TaskScheduler scheduler)
: base(model, scheduler, CommandPaletteHost.Instance)
{
ModelIsLoading = true;
IsInitialized = false;
}
}

View File

@@ -1,34 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
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;
}
}

View File

@@ -1,12 +0,0 @@
// 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.Messages;
/// <summary>
/// Used to perform a list item's secondary command when the user presses ctrl+enter in the search box
/// </summary>
public record ActivateSecondaryCommandMessage
{
}

View File

@@ -1,12 +0,0 @@
// 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.Messages;
/// <summary>
/// Used to perform a list item's command when the user presses enter in the search box
/// </summary>
public record ActivateSelectedListItemMessage
{
}

View File

@@ -1,7 +0,0 @@
// 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.Messages;
public record BeginInvokeMessage;

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record ClearSearchMessage()
{
}

View File

@@ -1,12 +0,0 @@
// 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.Messages;
/// <summary>
/// Used to announce that a context menu should close
/// </summary>
public record CloseContextMenuMessage
{
}

View File

@@ -1,7 +0,0 @@
// 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.Messages;
public record CmdPalInvokeResultMessage(Microsoft.CommandPalette.Extensions.CommandResultKind Kind);

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record DismissMessage()
{
}

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record FocusSearchBoxMessage()
{
}

View File

@@ -1,10 +0,0 @@
// 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.Messages;
public record GoBackMessage(bool WithAnimation = true, bool FocusSearch = true)
{
// TODO! sticking these properties here feels like leaking the UI into the models
}

View File

@@ -1,10 +0,0 @@
// 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.Messages;
// TODO! sticking these properties here feels like leaking the UI into the models
public record GoHomeMessage(bool WithAnimation = true, bool FocusSearch = true)
{
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record HandleCommandResultMessage(ExtensionObject<ICommandResult> Result)
{
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record HideDetailsMessage()
{
}

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record HotkeySummonMessage(string CommandId, IntPtr Hwnd)
{
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record LaunchUriMessage(Uri Uri)
{
}

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record NavigateBackMessage(bool FromBackspace = false)
{
}

View File

@@ -1,12 +0,0 @@
// 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.Messages;
/// <summary>
/// Used to navigate to the next command in the page when pressing the Down key in the SearchBox.
/// </summary>
public record NavigateNextCommand
{
}

View File

@@ -1,12 +0,0 @@
// 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.Messages;
/// <summary>
/// Used to navigate to the previous command in the page when pressing the Down key in the SearchBox.
/// </summary>
public record NavigatePreviousCommand
{
}

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation)
{
}

View File

@@ -1,22 +0,0 @@
// 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.UI.Xaml;
using Microsoft.UI.Xaml.Controls.Primitives;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
/// <summary>
/// Used to announce the context menu should open
/// </summary>
public record OpenContextMenuMessage(FrameworkElement? Element, FlyoutPlacementMode? FlyoutPlacementMode, Point? Point, ContextMenuFilterLocation ContextMenuFilterLocation)
{
}
public enum ContextMenuFilterLocation
{
Top,
Bottom,
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record OpenSettingsMessage()
{
}

View File

@@ -1,65 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
/// <summary>
/// Used to do a command - navigate to a page or invoke it
/// </summary>
public record PerformCommandMessage
{
public ExtensionObject<ICommand> Command { get; }
public object? Context { get; }
public bool WithAnimation { get; set; } = true;
public CommandPaletteHost? ExtensionHost { get; private set; }
public PerformCommandMessage(ExtensionObject<ICommand> command)
{
Command = command;
Context = null;
}
public PerformCommandMessage(TopLevelViewModel topLevelCommand)
{
Command = topLevelCommand.CommandViewModel.Model;
Context = null;
ExtensionHost = topLevelCommand.ExtensionHost;
}
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<IListItem> context)
{
Command = command;
Context = context.Unsafe;
}
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<ICommandItem> context)
{
Command = command;
Context = context.Unsafe;
}
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<ICommandContextItem> context)
{
Command = command;
Context = context.Unsafe;
}
public PerformCommandMessage(CommandContextItemViewModel contextCommand)
{
Command = contextCommand.Command.Model;
Context = contextCommand.Model.Unsafe;
}
public PerformCommandMessage(ConfirmResultViewModel vm)
{
Command = vm.PrimaryCommand.Model;
Context = null;
}
}

View File

@@ -1,14 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
/// <summary>
/// Message which closes the application. Used by <see cref="QuitCommand"/> via <see cref="BuiltInsCommandProvider"/>.
/// </summary>
public record QuitMessage()
{
}

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record ReloadCommandsMessage()
{
}

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record SettingsWindowClosedMessage
{
}

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record ShowConfirmationMessage(Microsoft.CommandPalette.Extensions.IConfirmationArgs? Args)
{
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record ShowDetailsMessage(DetailsViewModel Details)
{
}

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record ShowToastMessage(string Message)
{
}

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record ShowWindowMessage(IntPtr Hwnd)
{
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record TryCommandKeybindingMessage(bool Ctrl, bool Alt, bool Shift, bool Win, VirtualKey Key)
{
public bool Handled { get; set; }
}

View File

@@ -1,56 +0,0 @@
// 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.ComponentModel;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
/// <summary>
/// Used to update the command bar at the bottom to reflect the commands for a list item
/// </summary>
public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
{
}
public interface IContextMenuContext : INotifyPropertyChanged
{
public IEnumerable<IContextItemViewModel> MoreCommands { get; }
public bool HasMoreCommands { get; }
public List<IContextItemViewModel> AllCommands { get; }
/// <summary>
/// Generates a mapping of key -> command item for this particular item's
/// MoreCommands. (This won't include the primary Command, but it will
/// include the secondary one). This map can be used to quickly check if a
/// shortcut key was pressed
/// </summary>
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
/// that have a shortcut key set.</returns>
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
return MoreCommands
.OfType<CommandContextItemViewModel>()
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
}
// Represents everything the command bar needs to know about to show command
// buttons at the bottom.
//
// This is implemented by both ListItemViewModel and ContentPageViewModel,
// the two things with sub-commands.
public interface ICommandBarContext : IContextMenuContext
{
public string SecondaryCommandName { get; }
public CommandItemViewModel? PrimaryCommand { get; }
public CommandItemViewModel? SecondaryCommand { get; }
}

View File

@@ -1,9 +0,0 @@
// 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.Messages;
public record UpdateFallbackItemsMessage()
{
}

View File

@@ -56,6 +56,7 @@
<ItemGroup>
<ProjectReference Include="..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Core.ViewModels\Microsoft.CmdPal.Core.ViewModels.csproj" />
<ProjectReference Include="..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />

View File

@@ -1,14 +0,0 @@
// 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.Models;
public class ExtensionObject<T>(T? value) // where T : IInspectable
{
public T? Unsafe { get; } = value;
public override bool Equals(object? obj) => obj is ExtensionObject<T> ext && ext.Unsafe?.Equals(this.Unsafe) == true;
public override int GetHashCode() => Unsafe?.GetHashCode() ?? base.GetHashCode();
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -12,7 +12,6 @@ using Windows.Win32;
using Windows.Win32.System.Com;
using WinRT;
// [assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionWrapper : IExtensionWrapper

View File

@@ -1,267 +0,0 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
{
public TaskScheduler Scheduler { get; private set; }
private readonly ExtensionObject<IPage> _pageModel;
public bool IsLoading => ModelIsLoading || (!IsInitialized);
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsLoading))]
public virtual partial bool IsInitialized { get; protected set; }
[ObservableProperty]
public partial string ErrorMessage { get; protected set; } = string.Empty;
[ObservableProperty]
public partial bool IsNested { get; set; } = true;
// This is set from the SearchBar
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowSuggestion))]
public partial string Filter { get; set; } = string.Empty;
[ObservableProperty]
public virtual partial string PlaceholderText { get; private set; } = "Type here to search...";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowSuggestion))]
public virtual partial string TextToSuggest { get; protected set; } = string.Empty;
public bool ShowSuggestion => !string.IsNullOrEmpty(TextToSuggest) && TextToSuggest != Filter;
[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
// [ObservableProperty]s, because PropChanged comes in off the UI thread,
// and ObservableProperty is not smart enough to raise the PropertyChanged
// on the UI thread.
public string Name { get; protected set; } = string.Empty;
public string Title { get => string.IsNullOrEmpty(field) ? Name : field; protected set; } = string.Empty;
// This property maps to `IPage.IsLoading`, but we want to expose our own
// `IsLoading` property as a combo of this value and `IsInitialized`
public bool ModelIsLoading { get; protected set; } = true;
public IconInfoViewModel Icon { get; protected set; }
public PageViewModel(IPage? model, TaskScheduler scheduler, CommandPaletteHost extensionHost)
: base((IPageContext?)null)
{
_pageModel = new(model);
Scheduler = scheduler;
PageContext = new(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
[RelayCommand]
internal Task<bool> InitializeAsync()
{
// TODO: We may want a SemaphoreSlim lock here.
// TODO: We may want to investigate using some sort of AsyncEnumerable or populating these as they come into the UI layer
// Though we have to think about threading here and circling back to the UI thread with a TaskScheduler.
try
{
InitializeProperties();
}
catch (Exception ex)
{
ShowException(ex, _pageModel?.Unsafe?.Name);
return Task.FromResult(false);
}
// Notify we're done back on the UI Thread.
Task.Factory.StartNew(
() =>
{
IsInitialized = true;
// TODO: Do we want an event/signal here that the Page Views can listen to? (i.e. ListPage setting the selected index to 0, however, in async world the user may have already started navigating around page...)
},
CancellationToken.None,
TaskCreationOptions.None,
Scheduler);
return Task.FromResult(true);
}
public override void InitializeProperties()
{
var page = _pageModel.Unsafe;
if (page == null)
{
return; // throw?
}
Name = page.Name;
ModelIsLoading = page.IsLoading;
Title = page.Title;
Icon = new(page.Icon);
Icon.InitializeProperties();
// Let the UI know about our initial properties too.
UpdateProperty(nameof(Name));
UpdateProperty(nameof(Title));
UpdateProperty(nameof(ModelIsLoading));
UpdateProperty(nameof(IsLoading));
UpdateProperty(nameof(Icon));
page.PropChanged += Model_PropChanged;
}
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
var propName = args.PropertyName;
FetchProperty(propName);
}
catch (Exception ex)
{
ShowException(ex, _pageModel?.Unsafe?.Name);
}
}
partial void OnFilterChanged(string oldValue, string newValue) => OnFilterUpdated(newValue);
protected virtual void OnFilterUpdated(string filter)
{
// The base page has no notion of data, so we do nothing here...
// subclasses should override.
}
protected virtual void FetchProperty(string propertyName)
{
var model = this._pageModel.Unsafe;
if (model == null)
{
return; // throw?
}
var updateProperty = true;
switch (propertyName)
{
case nameof(Name):
this.Name = model.Name ?? string.Empty;
UpdateProperty(nameof(Title));
break;
case nameof(Title):
this.Title = model.Title ?? string.Empty;
break;
case nameof(IsLoading):
this.ModelIsLoading = model.IsLoading;
UpdateProperty(nameof(ModelIsLoading));
break;
case nameof(Icon):
this.Icon = new(model.Icon);
break;
default:
updateProperty = false;
break;
}
// GH #38829: If we always UpdateProperty here, then there's a possible
// race condition, where we raise the PropertyChanged(SearchText)
// before the subclass actually retrieves the new SearchText from the
// model. In that race situation, if the UI thread handles the
// PropertyChanged before ListViewModel fetches the SearchText, it'll
// think that the old search text is the _new_ value.
if (updateProperty)
{
UpdateProperty(propertyName);
}
}
public new void ShowException(Exception ex, string? extensionHint = null)
{
// Set the extensionHint to the Page Title (if we have one, and one not provided).
// extensionHint ??= _pageModel?.Unsafe?.Title;
extensionHint ??= ExtensionHost.Extension?.ExtensionDisplayName ?? Title;
Task.Factory.StartNew(
() =>
{
ErrorMessage += $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n";
},
CancellationToken.None,
TaskCreationOptions.None,
Scheduler);
}
public override string ToString() => $"{Title} ViewModel";
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
ExtensionHost.StatusMessages.CollectionChanged -= StatusMessages_CollectionChanged;
var model = _pageModel.Unsafe;
if (model != null)
{
model.PropChanged -= Model_PropChanged;
}
}
}
public interface IPageContext
{
void ShowException(Exception ex, string? extensionHint = null);
TaskScheduler Scheduler { get; }
}
public interface IPageViewModelFactoryService
{
/// <summary>
/// Creates a new instance of the page view model for the given page type.
/// </summary>
/// <param name="page">The page for which to create the view model.</param>
/// <param name="nested">Indicates whether the page is not the top-level page.</param>
/// <param name="host">The command palette host that will host the page (for status messages)</param>
/// <returns>A new instance of the page view model.</returns>
PageViewModel? TryCreatePageViewModel(IPage page, bool nested, CommandPaletteHost host);
}

View File

@@ -1,70 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ProgressViewModel : ExtensionObjectViewModel
{
public ExtensionObject<IProgressState> Model { get; }
public bool IsIndeterminate { get; private set; }
public uint ProgressPercent { get; private set; }
public ProgressViewModel(IProgressState progress, WeakReference<IPageContext> context)
: base(context)
{
Model = new(progress);
}
public override void InitializeProperties()
{
var model = Model.Unsafe;
if (model == null)
{
return; // throw?
}
IsIndeterminate = model.IsIndeterminate;
ProgressPercent = model.ProgressPercent;
model.PropChanged += Model_PropChanged;
}
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
FetchProperty(args.PropertyName);
}
catch (Exception ex)
{
ShowException(ex);
}
}
protected virtual void FetchProperty(string propertyName)
{
var model = this.Model.Unsafe;
if (model == null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(IsIndeterminate):
this.IsIndeterminate = model.IsIndeterminate;
break;
case nameof(ProgressPercent):
this.ProgressPercent = model.ProgressPercent;
break;
}
UpdateProperty(propertyName);
}
}

View File

@@ -6,7 +6,8 @@ using System.Diagnostics.CodeAnalysis;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Properties;
using Microsoft.Extensions.DependencyInjection;

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem
{
}

View File

@@ -1,467 +0,0 @@
// 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.Runtime.InteropServices;
using System.Runtime.Versioning;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using WinRT;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ShellViewModel : ObservableObject,
IRecipient<PerformCommandMessage>
{
private readonly IRootPageService _rootPageService;
private readonly TaskScheduler _scheduler;
private readonly IPageViewModelFactoryService _pageViewModelFactory;
private readonly Lock _invokeLock = new();
private Task? _handleInvokeTask;
[ObservableProperty]
public partial bool IsLoaded { get; set; } = false;
[ObservableProperty]
public partial DetailsViewModel? Details { get; set; }
[ObservableProperty]
public partial bool IsDetailsVisible { get; set; }
private PageViewModel _currentPage;
public PageViewModel CurrentPage
{
get => _currentPage;
set
{
var oldValue = _currentPage;
if (SetProperty(ref _currentPage, value))
{
if (oldValue is IDisposable disposable)
{
try
{
disposable.Dispose();
}
catch (Exception ex)
{
Logger.LogError(ex.ToString());
}
}
}
}
}
private IPage? _rootPage;
private IExtensionWrapper? _activeExtension;
private bool _isNested;
public bool IsNested { get => _isNested; }
public ShellViewModel(TaskScheduler scheduler, IRootPageService rootPageService, IPageViewModelFactoryService pageViewModelFactory)
{
_pageViewModelFactory = pageViewModelFactory;
_scheduler = scheduler;
_rootPageService = rootPageService;
_currentPage = new LoadingPageViewModel(null, _scheduler);
// Register to receive messages
WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
}
[RelayCommand]
public async Task<bool> LoadAsync()
{
// First, do any loading that the root page service needs to do before we can
// display the root page. For example, this might include loading
// the built-in commands, or loading the settings.
await _rootPageService.PreLoadAsync();
IsLoaded = true;
// Now that the basics are set up, we can load the root page.
_rootPage = _rootPageService.GetRootPage();
// This sends a message to us to load the root page view model.
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(new ExtensionObject<ICommand>(_rootPage)));
// Now that the root page is loaded, do any post-load work that the root page service needs to do.
// This runs asynchronously, on a background thread.
// This might include starting extensions, for example.
// Note: We don't await this, so that we can return immediately.
// This is important because we don't want to block the UI thread.
_ = Task.Run(async () =>
{
await _rootPageService.PostLoadRootPageAsync();
});
return true;
}
public void LoadPageViewModel(PageViewModel viewModel)
{
// Note: We removed the general loading state, extensions sometimes use their `IsLoading`, but it's inconsistently implemented it seems.
// IsInitialized is our main indicator of the general overall state of loading props/items from a page we use for the progress bar
// This triggers that load generally with the InitializeCommand asynchronously when we navigate to a page.
// We could re-track the page loading status, if we need it more granularly below again, but between the initialized and error message, we can infer some status.
// We could also maybe move this thread offloading we do for loading into PageViewModel and better communicate between the two... a few different options.
////LoadedState = ViewModelLoadedState.Loading;
if (!viewModel.IsInitialized
&& viewModel.InitializeCommand != null)
{
_ = Task.Run(async () =>
{
// You know, this creates the situation where we wait for
// both loading page properties, AND the items, before we
// display anything.
//
// We almost need to do an async await on initialize, then
// just a fire-and-forget on FetchItems.
// RE: We do set the CurrentPage in ShellPage.xaml.cs as well, so, we kind of are doing two different things here.
// Definitely some more clean-up to do, but at least its centralized to one spot now.
viewModel.InitializeCommand.Execute(null);
await viewModel.InitializeCommand.ExecutionTask!;
if (viewModel.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
// TODO: Handle failure case
if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
{
Logger.LogError(ex.ToString());
}
// TODO GH #239 switch back when using the new MD text block
// _ = _queue.EnqueueAsync(() =>
/*_queue.TryEnqueue(new(() =>
{
LoadedState = ViewModelLoadedState.Error;
}));*/
}
else
{
// TODO GH #239 switch back when using the new MD text block
// _ = _queue.EnqueueAsync(() =>
_ = Task.Factory.StartNew(
() =>
{
// bool f = await viewModel.InitializeCommand.ExecutionTask.;
// var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
// var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault<bool?>()!;
CurrentPage = viewModel; // result ? viewModel : null;
////LoadedState = result ? ViewModelLoadedState.Loaded : ViewModelLoadedState.Error;
},
CancellationToken.None,
TaskCreationOptions.None,
_scheduler);
}
});
}
else
{
CurrentPage = viewModel;
////LoadedState = ViewModelLoadedState.Loaded;
}
}
public void PerformTopLevelCommand(PerformCommandMessage message)
{
_rootPageService.OnPerformTopLevelCommand(message.Context);
}
public void Receive(PerformCommandMessage message)
{
PerformCommand(message);
}
private void PerformCommand(PerformCommandMessage message)
{
var command = message.Command.Unsafe;
if (command == null)
{
return;
}
if (!CurrentPage.IsNested)
{
// on the main page here
PerformTopLevelCommand(message);
}
IExtensionWrapper? extension = null;
try
{
// In the case that we're coming from a top-level command, the
// current page's host is the global instance. We only really want
// to use that as the host of last resort.
var pageHost = CurrentPage?.ExtensionHost;
if (pageHost == CommandPaletteHost.Instance)
{
pageHost = null;
}
var messageHost = message.ExtensionHost;
// Use the host from the current page if it has one, else use the
// one specified in the PerformMessage for a top-level command,
// else just use the global one.
CommandPaletteHost host;
// Top level items can come through without a Extension set on the
// message. In that case, the `Context` is actually the
// TopLevelViewModel itself, and we can use that to get at the
// extension object.
extension = pageHost?.Extension ?? messageHost?.Extension ?? null;
if (extension == null && message.Context is TopLevelViewModel topLevelViewModel)
{
extension = topLevelViewModel.ExtensionHost?.Extension;
host = pageHost ?? messageHost ?? topLevelViewModel?.ExtensionHost ?? CommandPaletteHost.Instance;
}
else
{
host = pageHost ?? messageHost ?? CommandPaletteHost.Instance;
}
if (extension != null)
{
if (messageHost != null)
{
Logger.LogDebug($"Activated top-level command from {extension.ExtensionDisplayName}");
}
else
{
Logger.LogDebug($"Activated command from {extension.ExtensionDisplayName}");
}
}
SetActiveExtension(extension);
if (command is IPage page)
{
Logger.LogDebug($"Navigating to page");
var isMainPage = command == _rootPage;
_isNested = !isMainPage;
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host);
if (pageViewModel == null)
{
Logger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
throw new NotSupportedException();
}
// Kick off async loading of our ViewModel
LoadPageViewModel(pageViewModel);
OnUIThread(() => { WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)); });
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation));
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
// See RootFrame_Navigated event handler.
}
else if (command is IInvokableCommand invokable)
{
Logger.LogDebug($"Invoking command");
WeakReferenceMessenger.Default.Send<BeginInvokeMessage>();
StartInvoke(message, invokable);
}
}
catch (Exception ex)
{
// TODO: It would be better to do this as a page exception, rather
// than a silent log message.
CommandPaletteHost.Instance.Log(ex.Message);
}
}
private void StartInvoke(PerformCommandMessage message, IInvokableCommand invokable)
{
// TODO GH #525 This needs more better locking.
lock (_invokeLock)
{
if (_handleInvokeTask != null)
{
// do nothing - a command is already doing a thing
}
else
{
_handleInvokeTask = Task.Run(() =>
{
SafeHandleInvokeCommandSynchronous(message, invokable);
});
}
}
}
private void SafeHandleInvokeCommandSynchronous(PerformCommandMessage message, IInvokableCommand invokable)
{
try
{
// Call out to extension process.
// * May fail!
// * May never return!
var result = invokable.Invoke(message.Context);
// But if it did succeed, we need to handle the result.
UnsafeHandleCommandResult(result);
_handleInvokeTask = null;
}
catch (Exception ex)
{
_handleInvokeTask = null;
// TODO: It would be better to do this as a page exception, rather
// than a silent log message.
CommandPaletteHost.Instance.Log(ex.Message);
}
}
private void UnsafeHandleCommandResult(ICommandResult? result)
{
if (result == null)
{
// No result, nothing to do.
return;
}
var kind = result.Kind;
Logger.LogDebug($"handling {kind.ToString()}");
WeakReferenceMessenger.Default.Send<CmdPalInvokeResultMessage>(new(kind));
switch (kind)
{
case CommandResultKind.Dismiss:
{
// Reset the palette to the main page and dismiss
GoHome(withAnimation: false, focusSearch: false);
WeakReferenceMessenger.Default.Send<DismissMessage>();
break;
}
case CommandResultKind.GoHome:
{
// Go back to the main page, but keep it open
GoHome();
break;
}
case CommandResultKind.GoBack:
{
GoBack();
break;
}
case CommandResultKind.Hide:
{
// Keep this page open, but hide the palette.
WeakReferenceMessenger.Default.Send<DismissMessage>();
break;
}
case CommandResultKind.KeepOpen:
{
// Do nothing.
break;
}
case CommandResultKind.Confirm:
{
if (result.Args is IConfirmationArgs a)
{
WeakReferenceMessenger.Default.Send<ShowConfirmationMessage>(new(a));
}
break;
}
case CommandResultKind.ShowToast:
{
if (result.Args is IToastArgs a)
{
WeakReferenceMessenger.Default.Send<ShowToastMessage>(new(a.Message));
UnsafeHandleCommandResult(a.Result);
}
break;
}
}
}
public void SetActiveExtension(IExtensionWrapper? extension)
{
if (extension != _activeExtension)
{
// There's not really a CoDisallowSetForegroundWindow, so we don't
// need to handle that
_activeExtension = extension;
var extensionWinRtObject = _activeExtension?.GetExtensionObject();
if (extensionWinRtObject != null)
{
try
{
unsafe
{
var winrtObj = (IWinRTObject)extensionWinRtObject;
var intPtr = winrtObj.NativeObject.ThisPtr;
var hr = Native.CoAllowSetForegroundWindow(intPtr);
if (hr != 0)
{
Logger.LogWarning($"Error giving foreground rights: 0x{hr.Value:X8}");
}
}
}
catch (Exception ex)
{
Logger.LogError(ex.ToString());
}
}
}
}
public void GoHome(bool withAnimation = true, bool focusSearch = true)
{
SetActiveExtension(null);
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(withAnimation, focusSearch));
}
public void GoBack(bool withAnimation = true, bool focusSearch = true)
{
WeakReferenceMessenger.Default.Send<GoBackMessage>(new(withAnimation, focusSearch));
}
// You may ask yourself, why aren't we using CsWin32 for this?
// The CsWin32 projected version includes some object marshalling, like so:
//
// HRESULT CoAllowSetForegroundWindow([MarshalAs(UnmanagedType.IUnknown)] object pUnk,...)
//
// And if you do it like that, then the IForegroundTransfer interface isn't marshalled correctly
internal sealed class Native
{
[DllImport("OLE32.dll", ExactSpelling = true)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[SupportedOSPlatform("windows5.0")]
internal static extern unsafe global::Windows.Win32.Foundation.HRESULT CoAllowSetForegroundWindow(nint pUnk, [Optional] void* lpvReserved);
}
private void OnUIThread(Action action)
{
_ = Task.Factory.StartNew(
action,
CancellationToken.None,
TaskCreationOptions.None,
_scheduler);
}
}

View File

@@ -1,99 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class StatusMessageViewModel : ExtensionObjectViewModel
{
public ExtensionObject<IStatusMessage> Model { get; }
public string Message { get; private set; } = string.Empty;
public MessageState State { get; private set; } = MessageState.Info;
public string ExtensionPfn { get; set; } = string.Empty;
public ProgressViewModel? Progress { get; private set; }
public bool HasProgress => Progress != null;
// public bool IsIndeterminate => Progress != null && Progress.IsIndeterminate;
// public double ProgressValue => (Progress?.ProgressPercent ?? 0) / 100.0;
public StatusMessageViewModel(IStatusMessage message, WeakReference<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;
var modelProgress = model.Progress;
if (modelProgress != null)
{
Progress = new(modelProgress, this.PageContext);
Progress.InitializeProperties();
UpdateProperty(nameof(HasProgress));
}
model.PropChanged += Model_PropChanged;
}
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
FetchProperty(args.PropertyName);
}
catch (Exception ex)
{
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;
case nameof(Progress):
var modelProgress = model.Progress;
if (modelProgress != null)
{
Progress = new(modelProgress, this.PageContext);
Progress.InitializeProperties();
}
else
{
Progress = null;
}
UpdateProperty(nameof(HasProgress));
break;
}
UpdateProperty(propertyName);
}
}

View File

@@ -1,49 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class TagViewModel(ITag _tag, WeakReference<IPageContext> context) : ExtensionObjectViewModel(context)
{
private readonly ExtensionObject<ITag> _tagModel = new(_tag);
public string ToolTip => string.IsNullOrEmpty(ModelToolTip) ? Text : ModelToolTip;
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
public string Text { get; private set; } = string.Empty;
public string ModelToolTip { get; private set; } = string.Empty;
public OptionalColor Foreground { get; private set; }
public OptionalColor Background { get; private set; }
public IconInfoViewModel Icon { get; private set; } = new(null);
public override void InitializeProperties()
{
var model = _tagModel.Unsafe;
if (model == null)
{
return;
}
Text = model.Text;
Foreground = model.Foreground;
Background = model.Background;
ModelToolTip = model.ToolTip;
Icon = new(model.Icon);
Icon.InitializeProperties();
UpdateProperty(nameof(Text));
UpdateProperty(nameof(Foreground));
UpdateProperty(nameof(Background));
UpdateProperty(nameof(ToolTip));
UpdateProperty(nameof(Icon));
}
}

View File

@@ -1,15 +0,0 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ToastViewModel : ObservableObject
{
[ObservableProperty]
public partial string ToastMessage { get; set; } = string.Empty;
}

View File

@@ -10,7 +10,8 @@ using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.DependencyInjection;

View File

@@ -6,7 +6,8 @@ using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -254,7 +255,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
private void UpdateTags()
{
List<Tag> tags = new();
List<Tag> tags = [];
if (Hotkey != null)
{
@@ -341,4 +342,9 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
return false;
}
public PerformCommandMessage GetPerformCommandMessage()
{
return new PerformCommandMessage(this.CommandViewModel.Model, new Core.ViewModels.Models.ExtensionObject<IListItem>(this));
}
}