okay so this DOES WORK it's just not in the right place at all

This commit is contained in:
Mike Griese
2026-02-06 13:51:14 -06:00
parent 0cae32df79
commit fd2df1c7e1
9 changed files with 221 additions and 73 deletions

View File

@@ -5,12 +5,13 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) :
CommandItemViewModel(new(contextItem), context),
ICommandContextItemViewModel
{
private readonly KeyChord nullKeyChord = new(0, 0, 0);
@@ -45,3 +46,16 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
contextItem.RequestedShortcut.ScanCode);
}
}
public interface ICommandContextItemViewModel : IContextItemViewModel
{
public string Title { get; }
public IconInfoViewModel Icon { get; }
public bool IsCritical { get; }
public KeyChord? RequestedShortcut { get; }
public bool HasRequestedShortcut { get; }
}

View File

@@ -10,7 +10,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class ListItemViewModel : CommandItemViewModel, IContextItemViewModel
public partial class ListItemViewModel : CommandItemViewModel, ICommandContextItemViewModel
{
public new ExtensionObject<IListItem> Model { get; }
@@ -61,6 +61,12 @@ public partial class ListItemViewModel : CommandItemViewModel, IContextItemViewM
}
}
bool ICommandContextItemViewModel.IsCritical => false;
KeyChord? ICommandContextItemViewModel.RequestedShortcut => null;
bool ICommandContextItemViewModel.HasRequestedShortcut => false;
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
: base(new(model), context)
{

View File

@@ -4,6 +4,7 @@
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Windows.Foundation;
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
@@ -20,12 +21,28 @@ public record PerformCommandMessage
public bool TransientPage { get; set; }
public bool OpenAsFlyout { get; private set; }
public Point? FlyoutPosition { get; private set; }
public PerformCommandMessage(ExtensionObject<ICommand> command)
{
Command = command;
Context = null;
}
public static PerformCommandMessage CreateFlyoutMessage(ExtensionObject<ICommand> command, Point flyoutPosition)
{
var message = new PerformCommandMessage(command)
{
WithAnimation = false,
TransientPage = true,
OpenAsFlyout = true,
FlyoutPosition = flyoutPosition,
};
return message;
}
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<IListItem> context)
{
Command = command;

View File

@@ -0,0 +1,11 @@
// 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.Models;
using Microsoft.CommandPalette.Extensions;
using Windows.Foundation;
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
public record ShowCommandInContextMenuMessage(IContextMenuContext Context, Point Position);

View File

@@ -10,6 +10,7 @@ using Microsoft.CmdPal.Core.Common;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Windows.Foundation;
namespace Microsoft.CmdPal.Core.ViewModels;
@@ -136,6 +137,28 @@ public partial class ShellViewModel : ObservableObject,
return true;
}
private async Task ShowPageAsFlyoutAsync(PageViewModel viewModel, Point flyoutPosition, CancellationToken cancellationToken = default)
{
if (viewModel is not IContextMenuContext ctx)
{
return;
}
// Instead of doing normal page navigation, we want to open this
// page as a flyout anchored to the given position.
var initialized = await InitializePageViewModelAsync(viewModel, cancellationToken);
if (initialized)
{
await SetCurrentPageAsync(viewModel, cancellationToken);
// send message
WeakReferenceMessenger.Default.Send(new ShowCommandInContextMenuMessage(ctx, flyoutPosition));
//// now cleanup navigation
// await CleanupNavigationTokenAsync(cancellationToken);
}
}
private async Task LoadPageViewModelAsync(PageViewModel viewModel, CancellationToken cancellationToken = default)
{
var initialized = await InitializePageViewModelAsync(viewModel, cancellationToken);
@@ -215,6 +238,15 @@ public partial class ShellViewModel : ObservableObject,
_scheduler);
}
private async Task CleanupNavigationTokenAsync(CancellationTokenSource cts)
{
// clean up the navigation token if it's still ours
if (Interlocked.CompareExchange(ref _navigationCts, null, cts) == cts)
{
cts.Dispose();
}
}
public void Receive(PerformCommandMessage message)
{
PerformCommand(message);
@@ -259,62 +291,7 @@ public partial class ShellViewModel : ObservableObject,
if (command is IPage page)
{
CoreLogger.LogDebug($"Navigating to page");
var isMainPage = command == _rootPage;
// Telemetry: Track extension page navigation for session metrics
if (host is not null)
{
var extensionId = host.GetExtensionDisplayName() ?? "builtin";
var commandId = command?.Id ?? "unknown";
var commandName = command?.Name ?? "unknown";
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
new(extensionId, commandId, commandName, true, 0));
}
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, !isMainPage, host!);
if (pageViewModel is null)
{
CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
throw new NotSupportedException();
}
// -------------------------------------------------------------
// Slice it here.
// Stuff above this, we need to always do, for both commands in the palette and flyout items
//
// Below here, this is all specific to navigating the current page of the palette
_isNested = !isMainPage;
_currentlyTransient = message.TransientPage;
pageViewModel.IsRootPage = isMainPage;
pageViewModel.HasBackButton = IsNested;
// Clear command bar, ViewModel initialization can already set new commands if it wants to
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
// Kick off async loading of our ViewModel
LoadPageViewModelAsync(pageViewModel, navigationToken)
.ContinueWith(
(Task t) =>
{
// clean up the navigation token if it's still ours
if (Interlocked.CompareExchange(ref _navigationCts, null, newCts) == newCts)
{
newCts.Dispose();
}
},
navigationToken,
TaskContinuationOptions.None,
_scheduler);
// While we're loading in the background, immediately move to the next page.
NavigateToPageMessage msg = new(pageViewModel, message.WithAnimation, navigationToken, message.TransientPage);
WeakReferenceMessenger.Default.Send(msg);
// 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.
StartOpenPage(message, page, host, newCts, navigationToken);
}
else if (command is IInvokableCommand invokable)
{
@@ -466,6 +443,71 @@ public partial class ShellViewModel : ObservableObject,
}
}
private void StartOpenPage(PerformCommandMessage message, IPage page, AppExtensionHost? host, CancellationTokenSource cts, CancellationToken navigationToken)
{
var isMainPage = page == _rootPage;
// Telemetry: Track extension page navigation for session metrics
// TODO! this block is unsafe
if (host is not null)
{
var extensionId = host.GetExtensionDisplayName() ?? "builtin";
var commandId = page.Id ?? "unknown";
var commandName = page.Name ?? "unknown";
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
new(extensionId, commandId, commandName, true, 0));
}
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, !isMainPage, host!);
if (pageViewModel is null)
{
CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
throw new NotSupportedException();
}
if (pageViewModel is ListViewModel listViewModel
&& message.OpenAsFlyout
&& message.FlyoutPosition is Point flyoutPosition)
{
ShowPageAsFlyoutAsync(listViewModel, flyoutPosition, navigationToken)
.ContinueWith(
(Task t) => CleanupNavigationTokenAsync(cts),
navigationToken,
TaskContinuationOptions.None,
_scheduler);
}
// -------------------------------------------------------------
// Slice it here.
// Stuff above this, we need to always do, for both commands in the palette and flyout items
//
// Below here, this is all specific to navigating the current page of the palette
_isNested = !isMainPage;
_currentlyTransient = message.TransientPage;
pageViewModel.IsRootPage = isMainPage;
pageViewModel.HasBackButton = IsNested;
// Clear command bar, ViewModel initialization can already set new commands if it wants to
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
// Kick off async loading of our ViewModel
LoadPageViewModelAsync(pageViewModel, navigationToken)
.ContinueWith(
(Task t) => CleanupNavigationTokenAsync(cts),
navigationToken,
TaskContinuationOptions.None,
_scheduler);
// While we're loading in the background, immediately move to the next page.
NavigateToPageMessage msg = new(pageViewModel, message.WithAnimation, navigationToken, message.TransientPage);
WeakReferenceMessenger.Default.Send(msg);
// 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.
}
public void GoHome(bool withAnimation = true, bool focusSearch = true)
{
_rootPageService.GoHome();

View File

@@ -29,7 +29,7 @@
Separator="{StaticResource SeparatorContextMenuViewModelTemplate}" />
<!-- Template for context items in the context item menu -->
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="coreViewModels:CommandContextItemViewModel">
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="coreViewModels:ICommandContextItemViewModel">
<Grid Padding="{StaticResource DefaultContextMenuItemPadding}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />

View File

@@ -11,7 +11,6 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -262,22 +261,19 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
try
{
// TODO! This is where we need to decide whether to open the command
// as a context menu or as a full page.
// as a context menu or as a full page.
//
// It might be the case that we should just have like... a
// PerformDockCommandMessage like PerformCommandMessage but with the
// context that we should be opening the command as a flyout.
PerformCommandMessage m = new(command.Model);
m.WithAnimation = false;
m.TransientPage = true;
// context that we should be opening the command as a flyout.
var m = PerformCommandMessage.CreateFlyoutMessage(command.Model, pos);
WeakReferenceMessenger.Default.Send(m);
var isPage = command.Model.Unsafe is not IInvokableCommand invokable;
if (isPage)
{
WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos));
}
// var isPage = command.Model.Unsafe is not IInvokableCommand invokable;
// if (isPage)
// {
// WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos));
// }
}
catch (COMException e)
{

View File

@@ -16,6 +16,29 @@
Closed="MainWindow_Closed"
mc:Ignorable="d">
<Grid x:Name="RootElement">
<Grid.Resources>
<Style
x:Name="ContextMenuFlyoutStyle"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Style.Setters>
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
</Style.Setters>
</Style>
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
<Flyout
x:Name="ContextMenuFlyout"
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
Opened="ContextMenuFlyout_Opened"
ShouldConstrainToRootBounds="False"
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
<controls:ContextMenu x:Name="ContextMenuControl" />
</Flyout>
</Grid.Resources>
<controls:BlurImageControl
HorizontalAlignment="Stretch"

View File

@@ -28,12 +28,12 @@ using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Graphics;
using Windows.System;
using Windows.UI;
using Windows.UI.WindowManagement;
using Windows.Win32;
using Windows.Win32.Foundation;
@@ -58,6 +58,8 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<NavigationDepthMessage>,
IRecipient<SearchQueryMessage>,
IRecipient<ErrorOccurredMessage>,
IRecipient<ShowCommandInContextMenuMessage>,
IRecipient<CloseContextMenuMessage>,
IRecipient<DragStartedMessage>,
IRecipient<DragCompletedMessage>,
IDisposable
@@ -142,6 +144,8 @@ public sealed partial class MainWindow : WindowEx,
WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(this);
WeakReferenceMessenger.Default.Register<SearchQueryMessage>(this);
WeakReferenceMessenger.Default.Register<ErrorOccurredMessage>(this);
WeakReferenceMessenger.Default.Register<ShowCommandInContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
@@ -676,6 +680,41 @@ public sealed partial class MainWindow : WindowEx,
_sessionErrorCount++;
}
public void Receive(ShowCommandInContextMenuMessage message)
{
DispatcherQueue.TryEnqueue(() =>
{
ContextMenuControl.ViewModel.SelectedItem = message.Context;
ContextMenuFlyout.ShouldConstrainToRootBounds = false;
ContextMenuFlyout.ShowMode = FlyoutShowMode.Standard;
ContextMenuFlyout.ShowAt(RootElement);
// ContextMenuFlyout.ShowAt(
// RootElement,
// new FlyoutShowOptions()
// {
// ShowMode = FlyoutShowMode.Standard,
// Position = message.Position,
// });
});
}
public void Receive(CloseContextMenuMessage message)
{
DispatcherQueue.TryEnqueue(() =>
{
if (ContextMenuFlyout.IsOpen)
{
ContextMenuFlyout.Hide();
}
});
}
private void ContextMenuFlyout_Opened(object sender, object e)
{
ContextMenuControl.FocusSearchBox();
}
/// <summary>
/// Ends the current telemetry session and emits the CmdPal_SessionDuration event.
/// Aggregates all session metrics collected since ShowWindow and sends them to telemetry.