mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 17:56:44 +02:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user