mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 12:11:09 +01:00
okay so this DOES WORK it's just not in the right place at all
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user