Continuing part deux

This commit is contained in:
Michael Jolley
2025-12-08 14:22:16 -06:00
parent 3928c609d9
commit 75f8d3affb
61 changed files with 2393 additions and 251 deletions

View File

@@ -4,17 +4,16 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class BeginInvokeEvent : TelemetryEventBase, IEvent
public class BeginInvokeEvent : EventBase, IEvent
{
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public BeginInvokeEvent()
{

View File

@@ -4,17 +4,16 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class ColdLaunchEvent : TelemetryEventBase, IEvent
public class ColdLaunchEvent : EventBase, IEvent
{
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public ColdLaunchEvent()
{

View File

@@ -4,15 +4,14 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class DismissedOnEscEvent : TelemetryEventBase, IEvent
public class DismissedOnEscEvent : EventBase, IEvent
{
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}

View File

@@ -4,15 +4,14 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class DismissedOnLostFocusEvent : TelemetryEventBase, IEvent
public class DismissedOnLostFocusEvent : EventBase, IEvent
{
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}

View File

@@ -4,15 +4,14 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class HotkeySummonedEvent : TelemetryEventBase, IEvent
public class HotkeySummonedEvent : EventBase, IEvent
{
public bool Global { get; set; }
@@ -21,5 +20,5 @@ public class HotkeySummonedEvent : TelemetryEventBase, IEvent
Global = global;
}
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}

View File

@@ -4,15 +4,14 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class OpenPageEvent : TelemetryEventBase, IEvent
public class OpenPageEvent : EventBase, IEvent
{
public int PageDepth { get; set; }
@@ -26,5 +25,5 @@ public class OpenPageEvent : TelemetryEventBase, IEvent
EventName = "CmdPal_OpenPage";
}
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}

View File

@@ -4,17 +4,16 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CommandPalette.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class OpenUriEvent : TelemetryEventBase, IEvent
public class OpenUriEvent : EventBase, IEvent
{
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Uri { get; set; }

View File

@@ -4,15 +4,14 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class ProcessStartedEvent : TelemetryEventBase, IEvent
public class ProcessStartedEvent : EventBase, IEvent
{
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}

View File

@@ -4,17 +4,16 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class ReactivateInstanceEvent : TelemetryEventBase, IEvent
public class ReactivateInstanceEvent : EventBase, IEvent
{
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public ReactivateInstanceEvent()
{

View File

@@ -4,17 +4,16 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CommandPalette.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class RunCommandEvent : TelemetryEventBase, IEvent
public class RunCommandEvent : EventBase, IEvent
{
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Command { get; set; }

View File

@@ -4,17 +4,16 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
namespace Microsoft.CommandPalette.UI.Models.Events;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class RunQueryEvent : TelemetryEventBase, IEvent
public class RunQueryEvent : EventBase, IEvent
{
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Query { get; set; }

View File

@@ -4,16 +4,16 @@
using Microsoft.Windows.ApplicationModel.Resources;
namespace Microsoft.CommandPalette.UI.Helpers;
namespace Microsoft.CommandPalette.UI.Models.Helpers;
internal static class ResourceLoaderInstance
public static class ResourceLoaderInstance
{
internal static ResourceLoader ResourceLoader { get; private set; }
public static ResourceLoader ResourceLoader { get; private set; }
static ResourceLoaderInstance()
{
ResourceLoader = new ResourceLoader("resources.pri");
}
internal static string GetString(string resourceId) => ResourceLoader.GetString(resourceId);
public static string GetString(string resourceId) => ResourceLoader.GetString(resourceId);
}

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
// 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.CommandPalette.UI.Models.Messages;
public record HandleCommandResultMessage() // ExtensionObject<ICommandResult> Result)
{
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
// 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.CommandPalette.UI.Models.Messages;
public record NavigateToPageMessage(); // PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken);

View File

@@ -0,0 +1,52 @@
// 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.CommandPalette.UI.Models.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 PerformCommandMessage() // ExtensionObject<ICommand> command)
{
// Command = command;
Context = null;
}
// public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<IListItem> context)
// {
// Command = command;
// Context = context.Unsafe;
// }
// public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<ICommandItem> context)
// {
// Command = command;
// Context = context.Unsafe;
// }
// public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<ICommandContextItem> context)
// {
// Command = command;
// Context = context.Unsafe;
// }
// public PerformCommandMessage(CommandContextItemViewModel contextCommand)
// {
// Command = contextCommand.Command.Model;
// Context = contextCommand.Model.Unsafe;
// }
// public PerformCommandMessage(ConfirmResultViewModel vm)
// {
// Command = vm.PrimaryCommand.Model;
// Context = null;
// }
}

View File

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

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.CommandPalette.Extensions;
namespace Microsoft.CommandPalette.UI.Models.Messages;
public record ShowConfirmationMessage(IConfirmationArgs? Args)
{
}

View File

@@ -0,0 +1,9 @@
// 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.CommandPalette.UI.Models.Messages;
public record ShowDetailsMessage() // DetailsViewModel Details)
{
}

View File

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

View File

@@ -0,0 +1,12 @@
// 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.CommandPalette.UI.Models.Messages;
public record TryCommandKeybindingMessage(bool Ctrl, bool Alt, bool Shift, bool Win, VirtualKey Key)
{
public bool Handled { get; set; }
}

View File

@@ -39,8 +39,4 @@
<ProjectReference Include="..\..\SDK\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Events\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CommandPalette</OutputPath>
<!-- For MVVM Toolkit Partial Properties/AOT support -->
<LangVersion>preview</LangVersion>
<!-- Disable SA1313 for Primary Constructor fields conflict https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors -->
<NoWarn>SA1313;</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Common" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\SDK\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,66 @@
GetPhysicallyInstalledSystemMemory
GlobalMemoryStatusEx
GetSystemInfo
GetForegroundWindow
SetForegroundWindow
GetWindowRect
GetCursorPos
SetWindowPos
HWND_TOPMOST
HWND_BOTTOM
IsIconic
RegisterHotKey
UnregisterHotKey
SetWindowLongPtr
CallWindowProc
ShowWindow
SetForegroundWindow
EnableWindow
IsWindowEnabled
SetFocus
SetActiveWindow
MonitorFromWindow
GetMonitorInfo
GetDpiForMonitor
WM_HOTKEY
WM_NCLBUTTONDBLCLK
Shell_NotifyIcon
LoadIcon
WM_USER
WM_WINDOWPOSCHANGING
RegisterWindowMessageW
ExtractIconEx
TRACK_POPUP_MENU_FLAGS
WM_COMMAND
WM_RBUTTONUP
WM_LBUTTONUP
WM_LBUTTONDBLCLK
CreatePopupMenu
TrackPopupMenuEx
InsertMenu
MessageBox
DwmGetWindowAttribute
DwmSetWindowAttribute
DWM_CLOAKED_APP
DWM_WINDOW_CORNER_PREFERENCE
CoWaitForMultipleObjects
INFINITE
CWMO_FLAGS
GetCurrentThreadId
SetWindowsHookEx
UnhookWindowsHookEx
CallNextHookEx
GetModuleHandle
GetWindowLong
SetWindowLong
WINDOW_EX_STYLE
CreateWindowEx
WNDCLASSEXW
RegisterClassEx
GetStockObject
GetModuleHandle

View File

@@ -8,14 +8,10 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CommandPalette.UI.Models;
using Microsoft.CommandPalette.UI.Models.Messages;
using Microsoft.UI.Xaml;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT.Interop;
using RS_ = Microsoft.CommandPalette.UI.Helpers.ResourceLoaderInstance;
using RS_ = Microsoft.CommandPalette.UI.Models.Helpers.ResourceLoaderInstance;
namespace Microsoft.CommandPalette.UI.Helpers;
namespace Microsoft.CommandPalette.UI.Services;
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_*")]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_*")]

View File

@@ -0,0 +1,12 @@
// 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.CommandPalette.UI.ViewModels;
/// <summary>
/// Encapsulates a navigation request within Command Palette view models.
/// </summary>
/// <param name="TargetViewModel">A view model that should be navigated to.</param>
/// <param name="NavigationToken"> A <see cref="CancellationToken"/> that can be used to cancel the pending navigation.</param>
public record AsyncNavigationRequest(object? TargetViewModel, CancellationToken NavigationToken);

View File

@@ -0,0 +1,12 @@
// 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;
namespace Microsoft.CommandPalette.UI.ViewModels;
public partial class PageViewModel : ObservableObject
{
public string Title { get; set; } = string.Empty;
}

View File

@@ -2,7 +2,11 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CommandPalette.UI.Models.Messages;
using Microsoft.Extensions.Logging;
namespace Microsoft.CommandPalette.ViewModels;
@@ -12,28 +16,381 @@ public partial class ShellViewModel : ObservableObject
private readonly ILogger logger;
private readonly TaskScheduler _scheduler;
// private IPage? _rootPage;
[ObservableProperty]
public partial bool IsLoaded { get; set; } = false;
private bool _isNested;
// private bool _isNested;
// public bool IsNested => _isNested;
// Cancellation token source for page loading/navigation operations
// private CancellationTokenSource? _navigationCts;
// [ObservableProperty]
// public partial DetailsViewModel? Details { get; set; }
[ObservableProperty]
public partial bool IsDetailsVisible { get; set; }
public bool IsNested => _isNested;
[ObservableProperty]
public partial bool IsSearchBoxVisible { get; set; } = true;
// private PageViewModel _currentPage;
// public PageViewModel CurrentPage
// {
// get => _currentPage;
// set
// {
// var oldValue = _currentPage;
// if (SetProperty(ref _currentPage, value))
// {
// oldValue.PropertyChanged -= CurrentPage_PropertyChanged;
// value.PropertyChanged += CurrentPage_PropertyChanged;
// if (oldValue is IDisposable disposable)
// {
// try
// {
// disposable.Dispose();
// }
// catch (Exception ex)
// {
// CoreLogger.LogError(ex.ToString());
// }
// }
// }
// }
// }
private void CurrentPage_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// if (e.PropertyName == nameof(PageViewModel.HasSearchBox))
// {
// IsSearchBoxVisible = CurrentPage.HasSearchBox;
// }
}
// public PageViewModel NullPage { get; private set; }
public ShellViewModel(
TaskScheduler scheduler,
ILogger logger)
// IRootPageService rootPageService,
// IPageViewModelFactoryService pageViewModelFactory,
// IAppHostService appHostService)
{
this.logger = logger;
_scheduler = scheduler;
_isNested = false;
this.logger = logger;
// _pageViewModelFactory = pageViewModelFactory;
// _rootPageService = rootPageService;
// _appHostService = appHostService;
// NullPage = new NullPageViewModel(_scheduler, appHostService.GetDefaultHost());
// _currentPage = new LoadingPageViewModel(null, _scheduler, appHostService.GetDefaultHost());
// Register to receive messages
// WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
// WeakReferenceMessenger.Default.Register<HandleCommandResultMessage>(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;
}
// private async Task LoadPageViewModelAsync(PageViewModel viewModel, CancellationToken cancellationToken = default)
// {
// // 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 is not null)
// {
// var outer = 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)
// {
// if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
// {
// CoreLogger.LogError(ex.ToString());
// }
// }
// else
// {
// var t = Task.Factory.StartNew(
// () =>
// {
// if (cancellationToken.IsCancellationRequested)
// {
// if (viewModel is IDisposable disposable)
// {
// try
// {
// disposable.Dispose();
// }
// catch (Exception ex)
// {
// CoreLogger.LogError(ex.ToString());
// }
// }
// return;
// }
// CurrentPage = viewModel;
// },
// cancellationToken,
// TaskCreationOptions.None,
// _scheduler);
// await t;
// }
// },
// cancellationToken);
// await outer;
// }
// else
// {
// if (cancellationToken.IsCancellationRequested)
// {
// if (viewModel is IDisposable disposable)
// {
// try
// {
// disposable.Dispose();
// }
// catch (Exception ex)
// {
// CoreLogger.LogError(ex.ToString());
// }
// }
// return;
// }
// CurrentPage = viewModel;
// }
// }
public void Receive(PerformCommandMessage message)
{
// PerformCommand(message);
}
// private void PerformCommand(PerformCommandMessage message)
// {
// // Create/replace the navigation cancellation token.
// // If one already exists, cancel and dispose it first.
// var newCts = new CancellationTokenSource();
// var oldCts = Interlocked.Exchange(ref _navigationCts, newCts);
// if (oldCts is not null)
// {
// try
// {
// oldCts.Cancel();
// }
// catch (Exception ex)
// {
// CoreLogger.LogError(ex.ToString());
// }
// finally
// {
// oldCts.Dispose();
// }
// }
// var navigationToken = newCts.Token;
// var command = message.Command.Unsafe;
// if (command is null)
// {
// return;
// }
// var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
// _rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host);
// try
// {
// if (command is IPage page)
// {
// CoreLogger.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 is null)
// {
// CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
// throw new NotSupportedException();
// }
// // 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.
// WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation, navigationToken));
// // 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)
// {
// CoreLogger.LogDebug($"Invoking command");
// WeakReferenceMessenger.Default.Send<BeginInvokeMessage>();
// StartInvoke(message, invokable, host);
// }
// }
// catch (Exception ex)
// {
// // TODO: It would be better to do this as a page exception, rather
// // than a silent log message.
// host?.Log(ex.Message);
// }
// }
// private void StartInvoke(PerformCommandMessage message, IInvokableCommand invokable, AppExtensionHost? host)
// {
// // TODO GH #525 This needs more better locking.
// lock (_invokeLock)
// {
// if (_handleInvokeTask is not null)
// {
// // do nothing - a command is already doing a thing
// }
// else
// {
// _handleInvokeTask = Task.Run(() =>
// {
// SafeHandleInvokeCommandSynchronous(message, invokable, host);
// });
// }
// }
// }
// private void SafeHandleInvokeCommandSynchronous(PerformCommandMessage message, IInvokableCommand invokable, AppExtensionHost? host)
// {
// 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.
// host?.Log(ex.Message);
// }
// }
// private void UnsafeHandleCommandResult(ICommandResult? result)
// {
// if (result is null)
// {
// // No result, nothing to do.
// return;
// }
// var kind = result.Kind;
// CoreLogger.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 GoHome(bool withAnimation = true, bool focusSearch = true)
{
// _rootPageService.GoHome();
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(withAnimation, focusSearch));
}
public void GoBack(bool withAnimation = true, bool focusSearch = true)
{
WeakReferenceMessenger.Default.Send<GoBackMessage>(new(withAnimation, focusSearch));
}
public void Receive(HandleCommandResultMessage message)
{
// UnsafeHandleCommandResult(message.Result.Unsafe);
}
private void OnUIThread(Action action)
@@ -44,4 +401,9 @@ public partial class ShellViewModel : ObservableObject
TaskCreationOptions.None,
_scheduler);
}
public void CancelNavigation()
{
// _navigationCts?.Cancel();
}
}

View File

@@ -2,6 +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 CommunityToolkit.Mvvm.ComponentModel;
namespace Microsoft.CommandPalette.UI.ViewModels;
public partial class ToastViewModel : ObservableObject

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CommandPalette.UI.Controls.CommandBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
Background="Transparent"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<converters:StringVisibilityConverter
x:Key="StringNotEmptyToVisibilityConverter"
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
<converters:BoolToVisibilityConverter
x:Key="BoolToInvertedVisibilityConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
<StackLayout
x:Name="VerticalStackLayout"
Orientation="Vertical"
Spacing="4" />
</ResourceDictionary>
</UserControl.Resources>
<Grid
MinHeight="40"
Padding="4"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
x:Name="IconRoot"
Margin="3,0,-5,0">
<Button
x:Name="StatusMessagesButton"
x:Uid="StatusMessagesButton"
Padding="4"
Style="{StaticResource SubtleButtonStyle}">
</Button>
</Grid>
<Button
x:Name="SettingsIconButton"
x:Uid="SettingsButton"
Style="{StaticResource SubtleButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
VerticalAlignment="Center"
FontSize="16"
Glyph="&#xE713;" />
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="Settings" />
</StackPanel>
</Button>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="Title here"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<StackPanel
Grid.Column="2"
Padding="0,0,4,0"
HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="4">
<Button
x:Name="PrimaryButton"
Padding="6,4,4,4"
AutomationProperties.AutomationId="PrimaryCommandButton"
AutomationProperties.Name="Title here"
Background="Transparent"
Style="{StaticResource SubtleButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock
MaxWidth="160"
VerticalAlignment="Center"
MaxLines="1"
Style="{StaticResource CaptionTextBlockStyle}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<Border Style="{StaticResource HotkeyStyle}">
<FontIcon Glyph="&#xE751;" Style="{StaticResource HotkeyFontIconStyle}" />
</Border>
</StackPanel>
</Button>
<Button
x:Name="SecondaryButton"
Padding="6,4,4,4"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
AutomationProperties.AutomationId="SecondaryCommandButton"
AutomationProperties.Name="Secondary"
Style="{StaticResource SubtleButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock
MaxWidth="160"
VerticalAlignment="Center"
MaxLines="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="Secondary"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<StackPanel Orientation="Horizontal" Spacing="4">
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="Ctrl" />
</Border>
<Border Style="{StaticResource HotkeyStyle}">
<FontIcon Glyph="&#xE751;" Style="{StaticResource HotkeyFontIconStyle}" />
</Border>
</StackPanel>
</StackPanel>
</Button>
<Button
x:Name="MoreCommandsButton"
x:Uid="MoreCommandsButton"
Padding="6,4,4,4"
AutomationProperties.AutomationId="MoreContextMenuButton"
Style="{StaticResource SubtleButtonStyle}"
ToolTipService.ToolTip="Ctrl+K">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="More"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap" />
<StackPanel Orientation="Horizontal" Spacing="4">
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="Ctrl" />
</Border>
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="K" />
</Border>
</StackPanel>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</UserControl>

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.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class CommandBar : UserControl
{
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CommandPalette.UI.Controls.SearchBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:Microsoft.CommandPalette.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<!-- Search box -->
<TextBox
x:Name="SearchBox"
MinHeight="32"
VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch"
AutomationProperties.AutomationId="MainSearchBox"
Style="{StaticResource SearchTextBoxStyle}" />
</UserControl>

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.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class SearchBar : UserControl
{
}

View File

@@ -0,0 +1,46 @@
// 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;
using Microsoft.UI.Xaml.Data;
using Windows.System;
namespace Microsoft.CommandPalette.UI.Converters;
public partial class KeyChordToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is KeyChord shortcut && (VirtualKey)shortcut.Vkey != VirtualKey.None)
{
var result = string.Empty;
if (shortcut.Modifiers.HasFlag(VirtualKeyModifiers.Control))
{
result += "Ctrl+";
}
if (shortcut.Modifiers.HasFlag(VirtualKeyModifiers.Shift))
{
result += "Shift+";
}
if (shortcut.Modifiers.HasFlag(VirtualKeyModifiers.Menu))
{
result += "Alt+";
}
result += (VirtualKey)shortcut.Vkey;
return result;
}
return string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,37 @@
// 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;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.CommandPalette.UI.Converters;
public partial class MessageStateToSeverityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is MessageState state)
{
switch (state)
{
case MessageState.Info:
return InfoBarSeverity.Informational;
case MessageState.Success:
return InfoBarSeverity.Success;
case MessageState.Warning:
return InfoBarSeverity.Warning;
case MessageState.Error:
return InfoBarSeverity.Error;
}
}
return InfoBarSeverity.Informational;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,23 @@
// 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.Data;
using RS_ = Microsoft.CommandPalette.UI.Helpers.ResourceLoaderInstance;
namespace Microsoft.CommandPalette.UI.Converters;
public partial class PlaceholderTextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return value is string placeholder && !string.IsNullOrEmpty(placeholder)
? placeholder
: (object)RS_.GetString("DefaultSearchPlaceholderText");
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,31 @@
// 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.CommandPalette.UI.Helpers.MarkdownImageProviders;
internal sealed partial class CompositeImageSourceProvider : IImageSourceProvider
{
private readonly IImageSourceProvider[] _imageProviders =
[
new HttpImageSourceProvider(),
new LocalImageSourceProvider(),
new DataImageSourceProvider()
];
public Task<ImageSourceInfo> GetImageSource(string url)
{
var provider = _imageProviders.FirstOrDefault(p => p.ShouldUseThisProvider(url));
if (provider == null)
{
throw new NotSupportedException($"No image provider found for URL: {url}");
}
return provider.GetImageSource(url);
}
public bool ShouldUseThisProvider(string url)
{
return _imageProviders.Any(provider => provider.ShouldUseThisProvider(url));
}
}

View File

@@ -0,0 +1,74 @@
// 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.Text;
using Windows.Storage.Streams;
namespace Microsoft.CommandPalette.UI.Helpers.MarkdownImageProviders;
internal sealed partial class DataImageSourceProvider : IImageSourceProvider
{
private readonly ImageSourceFactory.ImageDecodeOptions _decodeOptions;
public DataImageSourceProvider(ImageSourceFactory.ImageDecodeOptions? decodeOptions = null)
=> _decodeOptions = decodeOptions ?? new ImageSourceFactory.ImageDecodeOptions();
public bool ShouldUseThisProvider(string url)
=> url.StartsWith("data:", StringComparison.OrdinalIgnoreCase);
public async Task<ImageSourceInfo> GetImageSource(string url)
{
if (!ShouldUseThisProvider(url))
{
throw new ArgumentException("URL is not a data: URI.", nameof(url));
}
// data:[<media type>][;base64],<data>
var comma = url.IndexOf(',');
if (comma < 0)
{
throw new FormatException("Invalid data URI: missing comma separator.");
}
var header = url[5..comma]; // after "data:"
var payload = url[(comma + 1)..]; // after comma
// Parse header
string? contentType = null;
var isBase64 = false;
if (!string.IsNullOrEmpty(header))
{
var parts = header.Split(';');
// first token may be media type
if (!string.IsNullOrWhiteSpace(parts[0]) && parts[0].Contains('/'))
{
contentType = parts[0];
}
isBase64 = parts.Any(static p => p.Equals("base64", StringComparison.OrdinalIgnoreCase));
}
var bytes = isBase64
? Convert.FromBase64String(payload)
: Encoding.UTF8.GetBytes(Uri.UnescapeDataString(payload));
var mem = new InMemoryRandomAccessStream();
using (var writer = new DataWriter(mem.GetOutputStreamAt(0)))
{
writer.WriteBytes(bytes);
await writer.StoreAsync()!;
}
mem.Seek(0);
var imagePayload = new ImageSourceFactory.ImagePayload(mem, contentType, null);
var imageSource = await ImageSourceFactory.CreateAsync(imagePayload, _decodeOptions);
return new ImageSourceInfo(imageSource, new ImageHints
{
DownscaleOnly = true,
});
}
}

View File

@@ -0,0 +1,73 @@
// 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.WindowsRuntime;
using Windows.Storage.Streams;
namespace Microsoft.CommandPalette.UI.Helpers.MarkdownImageProviders;
/// <summary>
/// Implementation of IImageProvider to handle http/https images, but adds
/// a new functionality to handle image scaling.
/// </summary>
internal sealed partial class HttpImageSourceProvider : IImageSourceProvider
{
private readonly HttpClient _http;
public HttpImageSourceProvider(HttpClient? httpClient = null)
=> _http = httpClient ?? SharedHttpClient.Instance;
public bool ShouldUseThisProvider(string url)
=> Uri.TryCreate(url, UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
public async Task<ImageSourceInfo> GetImageSource(string url)
{
if (!ShouldUseThisProvider(url))
{
throw new ArgumentException("URL must be absolute http/https.", nameof(url));
}
using var req = new HttpRequestMessage(HttpMethod.Get, url);
req.Headers.TryAddWithoutValidation("Accept", "image/*,text/xml;q=0.9,application/xml;q=0.9,*/*;q=0.8");
using var resp = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
resp.EnsureSuccessStatusCode();
var contentType = resp.Content.Headers.ContentType?.MediaType;
using var mem = new InMemoryRandomAccessStream();
await CopyToRandomAccessStreamAsync(resp, mem);
var hints = ImageHints.ParseHintsFromUri(new Uri(url));
var imageSource = await ImageSourceFactory.CreateAsync(
new ImageSourceFactory.ImagePayload(mem, contentType, new Uri(url)),
new ImageSourceFactory.ImageDecodeOptions { SniffContent = true });
return new ImageSourceInfo(imageSource, hints);
}
private static async Task CopyToRandomAccessStreamAsync(HttpResponseMessage resp, InMemoryRandomAccessStream mem)
{
var data = await resp.Content.ReadAsByteArrayAsync();
await mem.WriteAsync(data.AsBuffer());
mem.Seek(0);
}
private static class SharedHttpClient
{
public static readonly HttpClient Instance = Create();
private static HttpClient Create()
{
var c = new HttpClient
{
Timeout = TimeSpan.FromSeconds(30),
};
c.DefaultRequestHeaders.UserAgent.ParseAdd("CommandPalette/1.0");
return c;
}
}
}

View File

@@ -0,0 +1,12 @@
// 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.CommandPalette.UI.Helpers.MarkdownImageProviders;
internal interface IImageSourceProvider
{
Task<ImageSourceInfo> GetImageSource(string url);
bool ShouldUseThisProvider(string url);
}

View File

@@ -0,0 +1,79 @@
// 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.CommandPalette.UI.Helpers.MarkdownImageProviders;
internal sealed class ImageHints
{
public static ImageHints Empty { get; } = new();
public double? DesiredPixelWidth { get; init; }
public double? DesiredPixelHeight { get; init; }
public double? MaxPixelWidth { get; init; }
public double? MaxPixelHeight { get; init; }
public bool? DownscaleOnly { get; init; }
public string? FitMode { get; init; } // fit=fit
public static ImageHints ParseHintsFromUri(Uri? uri)
{
if (uri is null || string.IsNullOrEmpty(uri.Query))
{
return Empty;
}
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var p in uri.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries))
{
var kv = p.Split('=', 2);
var k = Uri.UnescapeDataString(kv[0]);
var v = kv.Length > 1 ? Uri.UnescapeDataString(kv[1]) : string.Empty;
dict[k] = v;
}
return new ImageHints
{
DesiredPixelWidth = GetInt("--x-cmdpal-width"),
DesiredPixelHeight = GetInt("--x-cmdpal-height"),
MaxPixelWidth = GetInt("--x-cmdpal-maxwidth"),
MaxPixelHeight = GetInt("--x-cmdpal-maxheight"),
DownscaleOnly = GetBool("--x-cmdpal-downscaleOnly") ?? (GetBool("--x-cmdpal-upscale") is bool u ? !u : (bool?)null),
FitMode = dict.TryGetValue("--x-cmdpal-fit", out var f) ? f : null,
};
int? GetInt(params string[] keys)
{
foreach (var k in keys)
{
if (dict.TryGetValue(k, out var v) && int.TryParse(v, out var n))
{
return n;
}
}
return null;
}
bool? GetBool(params string[] keys)
{
foreach (var k in keys)
{
if (dict.TryGetValue(k, out var v) && (v.Equals("true", StringComparison.OrdinalIgnoreCase) || v == "1"))
{
return true;
}
else if (dict.TryGetValue(k, out var v2) && (v2.Equals("false", StringComparison.OrdinalIgnoreCase) || v2 == "0"))
{
return false;
}
}
return null;
}
}
}

View File

@@ -0,0 +1,145 @@
// 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.Globalization;
using System.Xml.Linq;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Foundation;
using Windows.Storage.Streams;
namespace Microsoft.CommandPalette.UI.Helpers.MarkdownImageProviders;
/// <summary>
/// Creates a new image source.
/// </summary>
internal static class ImageSourceFactory
{
private static DispatcherQueue? _dispatcherQueue;
public static void Initialize()
{
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
}
internal sealed record ImagePayload(
IRandomAccessStream Stream,
string? ContentType,
Uri? SourceUri);
internal sealed class ImageDecodeOptions
{
public bool SniffContent { get; init; } = true;
public int? DecodePixelWidth { get; init; }
public int? DecodePixelHeight { get; init; }
}
public static async Task<ImageSource> CreateAsync(ImagePayload payload, ImageDecodeOptions? options = null)
{
options ??= new ImageDecodeOptions();
var isSvg =
IsSvgByHeaderOrUrl(payload.ContentType, payload.SourceUri) ||
(options.SniffContent && SniffSvg(payload.Stream));
payload.Stream.Seek(0);
return await _dispatcherQueue!.EnqueueAsync(async () =>
{
if (isSvg)
{
var size = GetSvgSize(payload.Stream.AsStreamForRead());
payload.Stream.Seek(0);
var svg = new SvgImageSource();
await svg.SetSourceAsync(payload.Stream);
svg.RasterizePixelWidth = size.Width;
svg.RasterizePixelHeight = size.Height;
return svg;
}
else
{
var bmp = new BitmapImage();
if (options.DecodePixelWidth is int w and > 0)
{
bmp.DecodePixelWidth = w;
}
if (options.DecodePixelHeight is int h and > 0)
{
bmp.DecodePixelHeight = h;
}
await bmp.SetSourceAsync(payload.Stream);
return (ImageSource)bmp;
}
});
}
public static Size GetSvgSize(Stream stream)
{
// Parse the SVG string as an XML document
var svgDocument = XDocument.Load(stream);
// Get the root element of the document
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
var svgElement = svgDocument.Root;
// Get the height and width attributes of the root element
#pragma warning disable CS8602 // Dereference of a possibly null reference.
var heightAttribute = svgElement.Attribute("height");
#pragma warning restore CS8602 // Dereference of a possibly null reference.
var widthAttribute = svgElement.Attribute("width");
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
// Convert the attribute values to double
double.TryParse(heightAttribute?.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var height);
double.TryParse(widthAttribute?.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var width);
// Return the height and width as a tuple
return new(width, height);
}
private static bool IsSvgByHeaderOrUrl(string? contentType, Uri? uri)
{
if (!string.IsNullOrEmpty(contentType) &&
contentType.StartsWith("image/svg+xml", StringComparison.OrdinalIgnoreCase))
{
return true;
}
var s = uri?.ToString();
return !string.IsNullOrEmpty(s) && s.Contains(".svg", StringComparison.OrdinalIgnoreCase);
}
private static bool SniffSvg(IRandomAccessStream ras)
{
try
{
const int maxProbe = 1024;
ras.Seek(0);
var s = ras.AsStreamForRead();
var toRead = (int)Math.Min(ras.Size, maxProbe);
var buf = new byte[toRead];
var read = s.Read(buf, 0, toRead);
if (read <= 0)
{
return false;
}
var head = System.Text.Encoding.UTF8.GetString(buf, 0, read);
ras.Seek(0);
return head.Contains("<svg", StringComparison.OrdinalIgnoreCase);
}
catch
{
ras.Seek(0);
return false;
}
}
}

View File

@@ -0,0 +1,9 @@
// 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.Media;
namespace Microsoft.CommandPalette.UI.Helpers.MarkdownImageProviders;
internal sealed record ImageSourceInfo(ImageSource ImageSource, ImageHints Hints);

View File

@@ -0,0 +1,113 @@
// 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.Media;
using Windows.Storage.Streams;
namespace Microsoft.CommandPalette.UI.Helpers.MarkdownImageProviders;
internal sealed partial class LocalImageSourceProvider : IImageSourceProvider
{
private readonly ImageSourceFactory.ImageDecodeOptions _decodeOptions;
public LocalImageSourceProvider(ImageSourceFactory.ImageDecodeOptions? decodeOptions = null)
=> _decodeOptions = decodeOptions ?? new ImageSourceFactory.ImageDecodeOptions();
public bool ShouldUseThisProvider(string url)
{
if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri) && uri.IsAbsoluteUri)
{
var scheme = uri.Scheme.ToLowerInvariant();
return scheme is "file" or "ms-appx" or "ms-appdata";
}
return false;
}
public async Task<ImageSourceInfo> GetImageSource(string url)
{
if (!ShouldUseThisProvider(url))
{
throw new ArgumentException("Not a local URL/path (file, ms-appx, ms-appdata).", nameof(url));
}
// Absolute URI?
if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.IsAbsoluteUri)
{
var scheme = uri.Scheme.ToLowerInvariant();
var hints = ImageHints.ParseHintsFromUri(uri);
if (scheme is "ms-appx" or "ms-appdata")
{
// Load directly from the package/appdata URI
var rasRef = RandomAccessStreamReference.CreateFromUri(uri);
using var ras = await rasRef.OpenReadAsync();
var payload = new ImageSourceFactory.ImagePayload(ras, ImageContentTypeHelper.GuessFromPathOrUri(uri.AbsoluteUri), uri);
return new ImageSourceInfo(await ImageSourceFactory.CreateAsync(payload, _decodeOptions), hints);
}
if (scheme is "file")
{
var path = uri.LocalPath;
return new ImageSourceInfo(await FromFilePathAsync(path, uri, _decodeOptions), hints);
}
}
throw new InvalidOperationException("Unsupported local URL/path.");
}
private static async Task<ImageSource> FromFilePathAsync(string path, Uri sourceUri, ImageSourceFactory.ImageDecodeOptions decodeOptions)
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024, useAsync: true);
using var mem = new InMemoryRandomAccessStream();
using var outStream = mem.AsStreamForWrite();
await fs.CopyToAsync(outStream).ConfigureAwait(true);
await outStream.FlushAsync().ConfigureAwait(true);
mem.Seek(0);
var payload = new ImageSourceFactory.ImagePayload(mem, ImageContentTypeHelper.GuessFromPathOrUri(path), sourceUri);
return await ImageSourceFactory.CreateAsync(payload, decodeOptions).ConfigureAwait(true);
}
private static class ImageContentTypeHelper
{
public static string? GuessFromPathOrUri(string? pathOrUri)
{
if (string.IsNullOrEmpty(pathOrUri))
{
return null;
}
// Try to get extension from path/uri
var ext = Path.GetExtension(pathOrUri);
if (string.IsNullOrEmpty(ext))
{
// try query-less URI path portion
if (Uri.TryCreate(pathOrUri, UriKind.RelativeOrAbsolute, out var u))
{
ext = Path.GetExtension(u.IsAbsoluteUri ? u.AbsolutePath : u.OriginalString);
}
}
ext = ext?.Trim().ToLowerInvariant();
return ext switch
{
".svg" => "image/svg+xml",
".png" => "image/png",
".jpg" => "image/jpeg",
".jpeg" => "image/jpeg",
".gif" => "image/gif",
".webp" => "image/webp",
".bmp" => "image/bmp",
".ico" => "image/x-icon",
".tif" => "image/tiff",
".tiff" => "image/tiff",
".avif" => "image/avif",
_ => null,
};
}
}
}

View File

@@ -0,0 +1,274 @@
// 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;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Foundation;
namespace Microsoft.CommandPalette.UI.Helpers.MarkdownImageProviders;
/// <summary>
/// Creates a new image configured to behave well as an inline image in a RichTextBlock.
/// </summary>
internal static class RtbInlineImageFactory
{
public sealed class InlineImageOptions
{
public double? WidthDip { get; init; }
public double? HeightDip { get; init; }
public double? MaxWidthDip { get; init; }
public double? MaxHeightDip { get; init; }
public bool FitColumnWidth { get; init; } = true;
public bool DownscaleOnly { get; init; } = true;
public Stretch Stretch { get; init; } = Stretch.None;
}
internal static Image Create(ImageSource source, InlineImageOptions? options = null)
{
options ??= new InlineImageOptions();
var img = new Image
{
Source = source,
Stretch = options.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch,
};
// Track host RTB and subscribe once
RichTextBlock? rtb = null;
long paddingToken = 0;
var paddingSubscribed = false;
SizeChangedEventHandler? rtbSizeChangedHandler = null;
TypedEventHandler<XamlRoot, XamlRootChangedEventArgs>? xamlRootChangedHandler = null;
// If Source is replaced later, recompute
var sourceToken = img.RegisterPropertyChangedCallback(Image.SourceProperty!, (_, __) => Update());
img.Loaded += OnLoaded;
img.Unloaded += OnUnloaded;
img.ImageOpened += (_, __) => Update();
return img;
void OnLoaded(object? s, RoutedEventArgs e)
{
// store image initial Width and Height if they are force upon the image by
// MarkdownControl itself as result of parsing <img width="123" height="123" />
// If user sets Width/Height in options, that takes precedence
img.Tag ??= (img.Width, img.Height);
rtb ??= FindAncestor<RichTextBlock>(img);
if (rtb != null && !paddingSubscribed)
{
rtbSizeChangedHandler ??= (_, __) => Update();
rtb.SizeChanged += rtbSizeChangedHandler;
paddingToken = rtb.RegisterPropertyChangedCallback(Control.PaddingProperty!, (_, __) => Update());
paddingSubscribed = true;
}
if (img.XamlRoot != null)
{
xamlRootChangedHandler ??= (_, __) => Update();
img.XamlRoot.Changed += xamlRootChangedHandler;
}
Update();
}
void OnUnloaded(object? s, RoutedEventArgs e)
{
if (rtb != null && rtbSizeChangedHandler is not null)
{
rtb.SizeChanged -= rtbSizeChangedHandler;
}
if (rtb != null && paddingSubscribed)
{
rtb.UnregisterPropertyChangedCallback(Control.PaddingProperty!, paddingToken);
paddingSubscribed = false;
}
if (img.XamlRoot != null && xamlRootChangedHandler is not null)
{
img.XamlRoot.Changed -= xamlRootChangedHandler;
}
img.UnregisterPropertyChangedCallback(Image.SourceProperty!, sourceToken);
}
void Update()
{
if (rtb is null)
{
return;
}
double? externalWidth = null;
double? externalHeight = null;
if (img.Tag != null)
{
(externalWidth, externalHeight) = ((double, double))img.Tag;
}
var pad = rtb.Padding;
var columnDip = Math.Max(0.0, rtb.ActualWidth - pad.Left - pad.Right);
var scale = img.XamlRoot?.RasterizationScale is double s1 and > 0 ? s1 : 1.0;
var isSvg = img.Source is SvgImageSource;
var naturalPxW = GetNaturalPixelWidth(img.Source);
var naturalDipW = naturalPxW > 0 && naturalPxW != int.MaxValue ? naturalPxW / scale : double.PositiveInfinity; // SVG => ∞
double? desiredWidth = null;
if (externalWidth.HasValue && !double.IsNaN(externalWidth.Value))
{
img.Width = externalWidth.Value;
desiredWidth = externalWidth.Value;
}
else
{
if (options.WidthDip is double forcedW)
{
desiredWidth = options.DownscaleOnly && naturalPxW != int.MaxValue
? Math.Min(forcedW, naturalPxW)
: forcedW;
}
else if (options.FitColumnWidth)
{
desiredWidth = options.DownscaleOnly && naturalPxW != int.MaxValue
? Math.Min(columnDip, naturalPxW)
: columnDip;
}
else
{
desiredWidth = naturalPxW;
}
// Apply MaxWidth (never exceed column width by default)
double maxW;
var maxConstraint = options.FitColumnWidth ? columnDip : (isSvg ? 256 : double.PositiveInfinity);
if (options.MaxWidthDip.HasValue)
{
maxW = Math.Min(options.MaxWidthDip.Value, maxConstraint);
}
else if (options.DownscaleOnly)
{
maxW = Math.Min(naturalPxW, maxConstraint);
}
else
{
maxW = maxConstraint;
}
// Commit sizes
if (desiredWidth.HasValue)
{
img.Width = Math.Max(0, desiredWidth.Value);
}
img.MaxWidth = maxW > 0 ? maxW : maxConstraint;
}
if (externalHeight.HasValue && !double.IsNaN(externalHeight.Value))
{
img.Height = externalHeight.Value;
}
else
{
// ---- Height & MaxHeight ----
var desiredHeight = options.HeightDip;
var maxH = options.MaxHeightDip;
if (desiredHeight is double h)
{
img.Height = Math.Max(0, h);
}
if (maxH is double mh && mh > 0)
{
img.MaxHeight = mh;
}
}
if (options.FitColumnWidth
|| options.WidthDip is not null
|| options.HeightDip is not null
|| options.MaxWidthDip is not null
|| options.MaxHeightDip is not null
|| externalWidth.HasValue
|| externalHeight.HasValue)
{
img.Stretch = Stretch.Uniform;
}
else
{
img.Stretch = Stretch.None;
}
// Decode/rasterization hints
if (isSvg && img.Source is SvgImageSource svg)
{
var targetW = desiredWidth ?? Math.Min(img.MaxWidth, columnDip);
var pxW = Math.Max(1, (int)Math.Round(targetW * scale));
if ((int)svg.RasterizePixelWidth != pxW)
{
svg.RasterizePixelWidth = pxW;
}
if (options.HeightDip is double forcedH)
{
var pxH = Math.Max(1, (int)Math.Round(forcedH * scale));
if ((int)svg.RasterizePixelHeight != pxH)
{
svg.RasterizePixelHeight = pxH;
}
}
}
else if (img.Source is BitmapImage bi && naturalPxW > 0)
{
var widthToUse = desiredWidth ?? Math.Min(img.MaxWidth, columnDip);
if (widthToUse > 0)
{
var desiredPx = (int)Math.Round(Math.Min(naturalPxW, widthToUse * scale));
if (desiredPx > 0 && bi.DecodePixelWidth != desiredPx)
{
bi.DecodePixelWidth = desiredPx;
}
}
}
}
}
private static int GetNaturalPixelWidth(ImageSource? src) => src switch
{
BitmapSource bs when bs.PixelWidth > 0 => bs.PixelWidth, // raster
SvgImageSource sis => sis.RasterizePixelWidth > 0 ? (int)sis.RasterizePixelWidth : int.MaxValue, // vector => infinite
_ => 0,
};
private static T? FindAncestor<T>(DependencyObject start)
where T : DependencyObject
{
var cur = (DependencyObject?)start;
while (cur != null)
{
cur = VisualTreeHelper.GetParent(cur);
if (cur is T hit)
{
return hit;
}
}
return null;
}
}

View File

@@ -5,11 +5,10 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.Events;
using Microsoft.CommandPalette.UI.Controls;
using Microsoft.CommandPalette.UI.Helpers;
using Microsoft.CommandPalette.UI.Models;
using Microsoft.CommandPalette.UI.Models.Events;
using Microsoft.CommandPalette.UI.Models.Messages;
using Microsoft.CommandPalette.UI.Pages;
using Microsoft.CommandPalette.UI.ViewModels.Helpers;
@@ -230,7 +229,7 @@ public sealed partial class MainWindow : WindowEx,
return;
}
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi, logger);
AppWindow.MoveAndResize(newRect);
}
@@ -343,14 +342,13 @@ public sealed partial class MainWindow : WindowEx,
if (PInvoke.IsIconic(hwnd))
{
// Make sure our HWND is cloaked before any possible window manipulations
Cloak();
// Cloak();
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
}
if (target == MonitorBehavior.ToLast)
{
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi, logger);
AppWindow.MoveAndResize(newRect);
}
else
@@ -390,7 +388,7 @@ public sealed partial class MainWindow : WindowEx,
/// A window rectangle in physical pixels, moved to the nearest display and resized
/// if the DPI has changed.
/// </returns>
private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi)
private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi, ILogger logger)
{
var displayArea = DisplayArea.GetFromRect(windowRect, DisplayAreaFallback.Nearest);
if (displayArea is null)
@@ -405,7 +403,7 @@ public sealed partial class MainWindow : WindowEx,
return windowRect;
}
var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea);
var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea, logger);
if (originalDpi <= 0)
{
originalDpi = effectiveDpi; // use current DPI as baseline (no scaling adjustment needed)
@@ -462,7 +460,7 @@ public sealed partial class MainWindow : WindowEx,
return new RectInt32(targetX, targetY, targetWidth, targetHeight);
}
private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea)
private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea, ILogger logger)
{
var effectiveDpi = 96;
@@ -476,7 +474,7 @@ public sealed partial class MainWindow : WindowEx,
}
else
{
Logger.LogWarning($"GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {displayArea.DisplayId}");
Log_GetDpiForMonitorFailed(logger, hr, displayArea);
}
}
@@ -935,7 +933,7 @@ public sealed partial class MainWindow : WindowEx,
{
// ... then manually hide our window. When debugged, we won't get the cool cloaking,
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
Cloak();
// Cloak();
this.Hide();
return;
@@ -993,7 +991,7 @@ public sealed partial class MainWindow : WindowEx,
partial void Log_ExternalReloadDisabled();
[LoggerMessage(level: LogLevel.Warning, Message = "GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {DisplayArea.DisplayId}")]
partial void LogWarning_GetDpiForMonitorFailed(HRESULT hr, DisplayArea displayArea);
static partial void Log_GetDpiForMonitorFailed(ILogger logger, HRESULT hr, DisplayArea displayArea);
[LoggerMessage(level: LogLevel.Warning, Message = "DWM cloaking of the main window failed. HRESULT: {hr.Value}.")]
partial void Log_DwmCloakingFailed(HRESULT hr);

View File

@@ -164,14 +164,12 @@
<ProjectReference Include="..\Microsoft.CommandPalette.UI.Models\Microsoft.CommandPalette.UI.Models.csproj" />
<ProjectReference Include="..\Microsoft.CommandPalette.UI.ViewModels\Microsoft.CommandPalette.UI.ViewModels.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="ToastWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\ShellPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Folder Include="Selectors\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Microsoft.CommandPalette.UI.Pages.LoadingPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<ProgressRing
Width="36"
Height="36"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsIndeterminate="True" />
<TextBlock Text="Here we go"/>
</Grid>
</Page>

View File

@@ -0,0 +1,53 @@
// 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.UI.ViewModels;
using Microsoft.CommandPalette.ViewModels;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.CommandPalette.UI.Pages;
/// <summary>
/// We use this page to do initialization of our extensions and cache loading to hydrate our ViewModels.
/// </summary>
public sealed partial class LoadingPage : Page
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
public LoadingPage()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is not AsyncNavigationRequest request)
{
throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
}
if (request.TargetViewModel is not ShellViewModel shellVM)
{
throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ShellViewModel)}");
}
// This will load the built-in commands, then navigate to the main page.
// Once the mainpage loads, we'll start loading extensions.
shellVM.LoadCommand.Execute(null);
_ = Task.Run(async () =>
{
await shellVM.LoadCommand.ExecutionTask!;
if (shellVM.LoadCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
// TODO: Handle failure case
}
});
base.OnNavigatedTo(e);
}
}

View File

@@ -1,24 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Microsoft.CommandPalette.UI.Pages.ShellPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.CommandPalette.UI.Pages"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:cmdpalUI="using:Microsoft.CommandPalette.UI"
xmlns:cmdpalConverters="using:Microsoft.CommandPalette.UI.Converters"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:cpcontrols="using:Microsoft.CommandPalette.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CommandPalette.UI.Helpers"
xmlns:markdownImageProviders="using:Microsoft.CommandPalette.UI.Helpers.MarkdownImageProviders"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
Background="Transparent"
mc:Ignorable="d">
<!--xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock"-->
<Page.Resources>
<ResourceDictionary>
<converters:StringVisibilityConverter
x:Key="StringNotEmptyToVisibilityConverter"
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
<cmdpalConverters:MessageStateToSeverityConverter x:Key="MessageStateToSeverityConverter" />
<!--<selectors:DetailsDataTemplateSelector
x:Key="DetailsDataTemplateSelector"
CommandTemplate="{StaticResource DetailsCommandsTemplate}"
LinkTemplate="{StaticResource DetailsLinkTemplate}"
SeparatorTemplate="{StaticResource DetailsSeparatorTemplate}"
TagTemplate="{StaticResource DetailsTagsTemplate}" />-->
<converters:BoolToVisibilityConverter
x:Key="BoolToInvertedVisibilityConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
<Style
x:Key="DetailKeyTextBlockStyle"
BasedOn="{StaticResource CaptionTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
</Style>
<Style
x:Key="SeparatorKeyTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
</Style>
<Style
x:Key="DetailValueTextBlockStyle"
BasedOn="{StaticResource BodyTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
</Style>
<tkcontrols:MarkdownThemes
x:Key="DefaultMarkdownThemeConfig"
H3FontSize="12"
H3FontWeight="Normal" />
<!--<markdownImageProviders:ImageProvider x:Key="ImageProvider" />-->
<tkcontrols:MarkdownConfig
x:Key="DefaultMarkdownConfig"
ImageProvider="{StaticResource ImageProvider}"
Themes="{StaticResource DefaultMarkdownThemeConfig}" />
</ResourceDictionary>
</Page.Resources>
@@ -35,6 +90,340 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Back button and search box -->
<Grid
x:Name="TopBarGrid"
Padding="0,12,0,12"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Back button -->
<StackPanel Orientation="Horizontal">
<Image
Width="20"
Margin="20,0,6,0"
HorizontalAlignment="Center"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
AutomationProperties.AccessibilityView="Raw"
Source="ms-appx:///Assets/icon.svg">
<!--
Visibility="{x:Bind viewModel.CurrentPage.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}"-->
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
EasingMode="EaseIn"
EasingType="Cubic"
From="0"
To="1.0"
Duration="0:0:0.187" />
<animations:ScaleAnimation
EasingMode="EaseIn"
EasingType="Cubic"
From="0.5"
To="1"
Duration="0:0:0.187" />
</animations:Implicit.ShowAnimations>
<animations:Implicit.HideAnimations>
<animations:OpacityAnimation
EasingMode="EaseOut"
EasingType="Cubic"
From="1.0"
To="0"
Duration="0:0:0.187" />
<animations:ScaleAnimation
EasingMode="EaseOut"
EasingType="Cubic"
From="1"
To="0.5"
Duration="0:0:0.187" />
</animations:Implicit.HideAnimations>
</Image>
<!--<Button
x:Name="BackButton"
x:Uid="BackButton"
Margin="4,0,4,0"
Padding="4"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Click="BackButton_Clicked"
Content="{ui:FontIcon Glyph=&#xE76B;,
FontSize=14}"
FontSize="16"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind viewModel.CurrentPage.IsNested, Mode=OneWay}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
EasingMode="EaseIn"
EasingType="Cubic"
From="0"
To="1.0"
Duration="0:0:0.333" />
<animations:ScaleAnimation
From="0.5"
To="1"
Duration="0:0:0.333" />
<animations:TranslationAnimation
From="16,0,0"
To="0,0,0"
Duration="0:0:0.333" />
</animations:Implicit.ShowAnimations>
<animations:Implicit.HideAnimations>
<animations:OpacityAnimation
EasingMode="EaseOut"
EasingType="Cubic"
From="1.0"
To="0"
Duration="0:0:0.333" />
<animations:ScaleAnimation
EasingMode="EaseOut"
EasingType="Cubic"
From="1"
To="0.5"
Duration="0:0:0.333" />
<animations:TranslationAnimation
EasingMode="EaseOut"
EasingType="Cubic"
From="0,0,0"
To="16,0,0"
Duration="0:0:0.187" />
</animations:Implicit.HideAnimations>
</Button>-->
<!--<cpcontrols:IconBox
Grid.Column="1"
Width="20"
Margin="4,0,4,0"
VerticalAlignment="Center"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind viewModel.CurrentPage.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
Visibility="{x:Bind viewModel.CurrentPage.IsNested, Mode=OneWay}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
From="0"
To="1.0"
Duration="0:0:0.333" />
<animations:ScaleAnimation
From="0.8"
To="1"
Duration="0:0:0.333" />
<animations:TranslationAnimation
From="8,0,0"
To="0,0,0"
Duration="0:0:0.187" />
</animations:Implicit.ShowAnimations>
<animations:Implicit.HideAnimations>
<animations:OpacityAnimation
From="1.0"
To="0"
Duration="0:0:0.333" />
<animations:ScaleAnimation
From="1"
To="0.8"
Duration="0:0:0.333" />
<animations:TranslationAnimation
From="0,0,0"
To="8,0,0"
Duration="0:0:0.187" />
</animations:Implicit.HideAnimations>
</cpcontrols:IconBox>-->
</StackPanel>
<!-- Search box: wrapped in a grid to enable RepositionThemeTransitions -->
<Grid Grid.Column="1" HorizontalAlignment="Stretch">
<cpcontrols:SearchBar
x:Name="SearchBox"
HorizontalAlignment="Stretch" />
<!--CurrentPageviewModel="{x:Bind viewModel.CurrentPage, Mode=OneWay}" />-->
<Grid.Transitions>
<TransitionCollection>
<RepositionThemeTransition />
</TransitionCollection>
</Grid.Transitions>
</Grid>
</Grid>
<Grid x:Name="ContentGrid" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition x:Name="DetailsColumn" Width="Auto" />
</Grid.ColumnDefinitions>
<Frame
Name="RootFrame"
AutomationProperties.AutomationControlType="Pane"
AutomationProperties.LandmarkType="Navigation"
AutomationProperties.LiveSetting="Assertive"
IsNavigationStackEnabled="True" />
<ScrollViewer
x:Name="DetailsContent"
Grid.Column="1"
Margin="4"
HorizontalAlignment="Stretch"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource ControlCornerRadius}"
Visibility="Collapsed">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
From="0"
To="1.0"
Duration="0:0:0.187" />
<animations:TranslationAnimation
From="24,0,0"
To="0,0,0"
Duration="0:0:0.187" />
</animations:Implicit.ShowAnimations>
<animations:Implicit.HideAnimations>
<animations:OpacityAnimation
From="1.0"
To="0"
Duration="0:0:0.187" />
<animations:TranslationAnimation
From="0,0,0"
To="24,0,0"
Duration="0:0:0.187" />
</animations:Implicit.HideAnimations>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!--<cpcontrols:IconBox
x:Name="HeroImageBorder"
Width="64"
Margin="0,0,16,16"
HorizontalAlignment="Left"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind viewModel.Details.HeroImage, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
Visibility="{x:Bind HasHeroImage, Mode=OneWay}" />-->
<!--<TextBlock
Grid.Row="1"
FontSize="18"
FontWeight="SemiBold"
Text="{x:Bind viewModel.Details.Title, Mode=OneWay}"
TextWrapping="WrapWholeWords"
Visibility="{x:Bind viewModel.Details.Title, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}" />
<tkcontrols:MarkdownTextBlock
Grid.Row="2"
Margin="0,4,0,24"
Background="Transparent"
Config="{StaticResource DefaultMarkdownConfig}"
Text="{x:Bind viewModel.Details.Body, Mode=OneWay}"
UseEmphasisExtras="True"
UsePipeTables="True" />
<ItemsRepeater
Grid.Row="3"
ItemTemplate="{StaticResource DetailsDataTemplateSelector}"
ItemsSource="{x:Bind viewModel.Details.Metadata, Mode=OneWay}">
<ItemsRepeater.Layout>
<StackLayout Spacing="12" />
</ItemsRepeater.Layout>
</ItemsRepeater>-->
</Grid>
</ScrollViewer>
<!-- /DetailsContent -->
</Grid>
<!--<ScrollView
Grid.Row="2"
Grid.ColumnSpan="2"
MaxHeight="120"
Background="{ThemeResource SystemControlErrorBackgroundColor}"
BorderBrush="{ThemeResource SystemControlErrorTextForegroundBrush}"
BorderThickness="0,1,0,1"
CornerRadius="0"
Visibility="{x:Bind viewModel.CurrentPage.ErrorMessage, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}">
<TextBlock Margin="16,8,16,16" IsTextSelectionEnabled="True">
<Run
FontWeight="SemiBold"
Foreground="{ThemeResource SystemErrorTextColor}"
Text="Error(s) on page:" />
<LineBreak /><Run Text="{x:Bind viewModel.CurrentPage.ErrorMessage, Mode=OneWay}" />
</TextBlock>
</ScrollView>-->
</Grid>
<!--
Horrifying: You may ask yourself - why is there a Background on this InfoBar?
Well, as it turns out, the Informational InfoBar has a transparent
background. It just cannot be bothered. So, we need to manually give
it one to actually obscure the text beneath it. And you can't just give
the InfoBar itself a Background, because then the other Severity's
won't get colorized.
See https://github.com/microsoft/microsoft-ui-xaml/issues/5741
-->
<!--<StackPanel
Grid.Row="0"
Margin="16,8,16,8"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
CornerRadius="{ThemeResource ControlCornerRadius}">
<InfoBar
CornerRadius="{ThemeResource ControlCornerRadius}"
IsOpen="{x:Bind viewModel.CurrentPage.HasStatusMessage, Mode=OneWay}"
Message="{x:Bind viewModel.CurrentPage.MostRecentStatusMessage.Message, Mode=OneWay}"
Severity="{x:Bind viewModel.CurrentPage.MostRecentStatusMessage.State, Mode=OneWay, Converter={StaticResource MessageStateToSeverityConverter}}">
<InfoBar.Content>
<ProgressBar
Margin="0,-20,0,0"
IsIndeterminate="{x:Bind viewModel.CurrentPage.MostRecentStatusMessage.Progress.IsIndeterminate, Mode=OneWay}"
Visibility="{x:Bind viewModel.CurrentPage.MostRecentStatusMessage.HasProgress, Mode=OneWay}"
Value="{x:Bind viewModel.CurrentPage.MostRecentStatusMessage.Progress.ProgressPercent, Mode=OneWay}" />
--><!-- Margin="0,0,0,6" MaxWidth="200"/> --><!--
</InfoBar.Content>
</InfoBar>
</StackPanel>-->
<Grid
Grid.Row="1"
Background="{ThemeResource LayerOnAcrylicSecondaryBackgroundBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,1,0,0">
<cpcontrols:CommandBar />
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<!--
Why disable it and make it transparent?
To prevent the container from collapsing, ensuring the layout metrics remain unchanged.
-->
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.187" To="SearchBarCollapsed" />
</VisualStateGroup.Transitions>
<VisualState x:Name="SearchBarVisible" />
<!--<VisualState x:Name="SearchBarCollapsed">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind h:BindTransformers.Negate(viewModel.IsSearchBoxVisible), Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SearchBox.IsEnabled" Value="False" />
<Setter Target="SearchBox.Opacity" Value="0" />
<Setter Target="TopBarGrid.BorderBrush" Value="Transparent" />
</VisualState.Setters>
</VisualState>-->
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</Page>

View File

@@ -2,38 +2,129 @@
// 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 System.Text;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CommandPalette.UI.Helpers;
using Microsoft.CommandPalette.UI.Models;
using Microsoft.CommandPalette.UI.Models.Messages;
using Microsoft.CommandPalette.UI.ViewModels;
using Microsoft.CommandPalette.ViewModels;
using Windows.System;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
namespace Microsoft.CommandPalette.UI.Pages;
public sealed partial class ShellPage : Page
public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page
{
private readonly ShellViewModel viewModel;
private readonly SettingsModel _settingsModel;
private readonly ILogger logger;
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private readonly Microsoft.UI.Dispatching.DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
private readonly SlideNavigationTransitionInfo _slideRightTransition = new() { Effect = SlideNavigationTransitionEffect.FromRight };
private readonly SuppressNavigationTransitionInfo _noAnimation = new();
private readonly ToastWindow _toast = new();
private readonly CompositeFormat _pageNavigatedAnnouncement;
private CancellationTokenSource? _focusAfterLoadedCts;
public event PropertyChangedEventHandler? PropertyChanged;
private WeakReference<Page>? _lastNavigatedPageRef;
private SettingsWindow? _settingsWindow;
public ShellPage(ShellViewModel viewModel)
public ShellPage(ShellViewModel viewModel, SettingsModel settingsModel, ILogger logger)
{
InitializeComponent();
this.viewModel = viewModel;
_settingsModel = settingsModel;
this.logger = logger;
// AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
// AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
// AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
RootFrame.Navigate(typeof(LoadingPage), new AsyncNavigationRequest(viewModel, CancellationToken.None));
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
}
/// <summary>
/// Gets the default page animation, depending on the settings
/// </summary>
private NavigationTransitionInfo DefaultPageAnimation
{
get
{
return _settingsModel.DisableAnimations ? _noAnimation : _slideRightTransition;
}
}
private void SummonOnUiThread(HotkeySummonMessage message)
{
var commandId = message.CommandId;
var isRoot = string.IsNullOrEmpty(commandId);
if (isRoot)
{
// If this is the hotkey for the root level, then always show us
WeakReferenceMessenger.Default.Send<ShowWindowMessage>(new(message.Hwnd));
// Depending on the settings, either
// * Go home, or
// * Select the search text (if we should remain open on this page)
if (_settingsModel.AutoGoHomeInterval == TimeSpan.Zero)
{
// GoHome(false);
}
else if (_settingsModel.HighlightSearchOnActivate)
{
// SearchBox.SelectSearch();
}
}
// else
// {
// try
// {
// // For a hotkey bound to a command, first lookup the
// // command from our list of toplevel commands.
// var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
// var topLevelCommand = tlcManager.LookupCommand(commandId);
// if (topLevelCommand is not null)
// {
// var command = topLevelCommand.CommandViewModel.Model.Unsafe;
// var isPage = command is not IInvokableCommand;
// // If the bound command is an invokable command, then
// // we don't want to open the window at all - we want to
// // just do it.
// if (isPage)
// {
// // If we're here, then the bound command was a page
// // of some kind. Let's pop the stack, show the window, and navigate to it.
// GoHome(false);
// WeakReferenceMessenger.Default.Send<ShowWindowMessage>(new(message.Hwnd));
// }
// var msg = topLevelCommand.GetPerformCommandMessage();
// msg.WithAnimation = false;
// WeakReferenceMessenger.Default.Send<PerformCommandMessage>(msg);
// // we can't necessarily SelectSearch() here, because when the page is loaded,
// // we'll fetch the SearchText from the page itself, and that'll stomp the
// // selection we start now.
// // That's probably okay though.
// }
// }
// catch
// {
// }
// }
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
private void BackButton_Clicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
[LoggerMessage(Level = LogLevel.Error)]
partial void Log_ShowConfirmationMessageError(Exception ex);
[LoggerMessage(Level = LogLevel.Warning, Message = "Invalid navigation target: AsyncNavigationRequest.{targetViewModel} must be {pageViewModel}")]
partial void Log_InvalidNavigationTarget(string targetViewModel, string pageViewModel);
[LoggerMessage(Level = LogLevel.Warning, Message = "Unrecognized target for shell navigation: {parameter}")]
partial void Log_UnrecognizedNavigationTarget(NavigationEventArgs parameter);
}

View File

@@ -2,10 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.Events;
using Microsoft.CommandPalette.UI.Models.Events;
using Microsoft.CommandPalette.UI.Services;
using Microsoft.CommandPalette.UI.Services.Telemetry;
using Microsoft.Extensions.Logging;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Dispatching;
using Microsoft.Windows.AppLifecycle;
using Windows.Win32;
@@ -38,7 +38,7 @@ internal sealed partial class Program
}
Log_AppStart(logger, DateTime.UtcNow);
TelemetryService.WriteEvent(new ProcessStartedEvent());
PowerToysTelemetry.Log.WriteEvent(new ProcessStartedEvent());
WinRT.ComWrappersSupport.InitializeComWrappers();
var isRedirect = DecideRedirection();
@@ -63,13 +63,13 @@ internal sealed partial class Program
if (keyInstance.IsCurrent)
{
TelemetryService.WriteEvent(new ColdLaunchEvent());
PowerToysTelemetry.Log.WriteEvent(new ColdLaunchEvent());
keyInstance.Activated += OnActivated;
}
else
{
isRedirect = true;
TelemetryService.WriteEvent(new ReactivateInstanceEvent());
PowerToysTelemetry.Log.WriteEvent(new ReactivateInstanceEvent());
RedirectActivationTo(args, keyInstance);
}

View File

@@ -1,14 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CommandPalette.UI.Services.Telemetry;
public abstract class TelemetryEventBase : EventBase, IEvent
{
// Overridden in derived classes
public abstract PartA_PrivTags PartA_PrivTags { get; }
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Telemetry;
namespace Microsoft.CommandPalette.UI.Services.Telemetry;
internal static class TelemetryService
{
public static void WriteEvent(TelemetryEventBase telemetryEvent) => PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
}

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<winuiex:WindowEx
x:Class="Microsoft.CommandPalette.UI.ToastWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.CommandPalette.UI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:winuiex="using:WinUIEx"
Title="Command Palette Toast"
mc:Ignorable="d">
<winuiex:WindowEx.SystemBackdrop>
<DesktopAcrylicBackdrop />
</winuiex:WindowEx.SystemBackdrop>
<Grid x:Name="ToastGrid">
<!-- This padding is used to calculate the dimensions of the ToastWindow -->
<TextBlock
x:Name="ToastText"
Padding="12,12,24,20"
Text="{x:Bind ViewModel.ToastMessage, Mode=OneWay}"
TextAlignment="Center" />
</Grid>
</winuiex:WindowEx>

View File

@@ -1,97 +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 ManagedCommon;
using Microsoft.CommandPalette.UI.Models.Messages;
using Windows.System;
using RS_ = Microsoft.CommandPalette.UI.Helpers.ResourceLoaderInstance;
namespace Microsoft.CommandPalette.UI;
public sealed partial class ToastWindow : WindowEx,
IRecipient<QuitMessage>
{
private readonly HWND _hwnd;
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private readonly HiddenOwnerWindowBehavior _hiddenOwnerWindowBehavior = new();
public ToastViewModel ViewModel { get; } = new();
public ToastWindow()
{
this.InitializeComponent();
AppWindow.Hide();
ExtendsContentIntoTitleBar = true;
AppWindow.SetPresenter(AppWindowPresenterKind.CompactOverlay);
this.SetIcon();
AppWindow.Title = RS_.GetString("ToastWindowTitle");
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
_hiddenOwnerWindowBehavior.ShowInTaskbar(this, false);
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
PInvoke.EnableWindow(_hwnd, false);
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
}
private static double GetScaleFactor(HWND hwnd)
{
try
{
var monitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
_ = PInvoke.GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
return dpiX / 96.0;
}
catch (Exception ex)
{
Logger.LogError($"Failed to get scale factor, error: {ex.Message}");
return 1.0;
}
}
private void PositionCentered()
{
this.SetWindowSize(ToastText.ActualWidth, ToastText.ActualHeight);
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest);
if (displayArea is not null)
{
var centeredPosition = AppWindow.Position;
centeredPosition.X = (displayArea.WorkArea.Width - AppWindow.Size.Width) / 2;
var monitorHeight = displayArea.WorkArea.Height;
var windowHeight = AppWindow.Size.Height;
centeredPosition.Y = monitorHeight - (windowHeight + 8); // Align with other shell toasts, like the volume indicator.
AppWindow.Move(centeredPosition);
}
}
public void ShowToast(string message)
{
ViewModel.ToastMessage = message;
DispatcherQueue.TryEnqueue(
DispatcherQueuePriority.Low,
() =>
{
PositionCentered();
// SW_SHOWNA prevents us from getting activated (and stealing FG)
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_SHOWNA);
_debounceTimer.Debounce(
() =>
{
AppWindow.Hide();
},
interval: TimeSpan.FromMilliseconds(2500),
immediate: false);
});
}
public void Receive(QuitMessage message)
{
// This might come in on a background thread
DispatcherQueue.TryEnqueue(() => Close());
}
}