From 70bf430d9f4bbc857f42ad616de9b0cef2c078eb Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 27 Feb 2026 07:24:23 -0600 Subject: [PATCH] CmdPal: Add a dock (#45824) Add support for a "dock" window in CmdPal. The dock is a toolbar powered by the `APPBAR` APIs. This gives you a persistent region to display commands for quick shortcuts or glanceable widgets. The dock can be pinned to any side of the screen. The dock can be independently styled with any of the theming controls cmdpal already has The dock has three "regions" to pin to - the "start", the "center", and the "end". Elements on the dock are grouped as "bands", which contains a set of "items". Each "band" is one atomic unit. For example, the Media Player extension produces 4 items, but one _band_. The dock has only one size (for now) The dock will only appear on your primary display (for now) This PR includes support for pinning arbitrary top-level commands to the dock - however, we're planning on replacing that with a more universal ability to pin any command to the dock or top level. (see #45191). This is at least usable for now. This is definitely still _even more preview_ than usual PowerToys features, but it's more than usable. I'd love to get it out there and start collecting feedback on where to improve next. I'll probably add a follow-up issue for tracking the remaining bugs & nits. closes #45201 --------- Co-authored-by: Niels Laute --- .github/actions/spell-check/allow/code.txt | 12 + src/modules/cmdpal/.wt.json | 5 + .../Microsoft.CmdPal.Common/CoreLogger.cs | 4 +- .../Helpers/PinnedDockItem.cs | 24 + .../Properties/Resources.Designer.cs | 9 + .../Properties/Resources.resx | 8 +- .../AppearanceSettingsViewModel.cs | 4 +- .../CommandBarViewModel.cs | 7 +- .../CommandItemViewModel.cs | 36 +- .../CommandPalettePageViewModelFactory.cs | 2 +- .../CommandProviderWrapper.cs | 195 ++++- .../Commands/BuiltInsCommandProvider.cs | 19 +- .../Commands/MainListPage.cs | 1 + .../Commands/OpenSettingsCommand.cs | 2 +- .../ContentFormViewModel.cs | 46 +- .../ContextMenuViewModel.cs | 7 +- .../DefaultContextMenuFactory.cs | 8 + .../Dock/DockBandSettingsViewModel.cs | 251 +++++++ .../Dock/DockBandViewModel.cs | 300 ++++++++ .../Dock/DockViewModel.cs | 633 ++++++++++++++++ .../Dock/DockWindowViewModel.cs | 90 +++ .../DockAppearanceSettingsViewModel.cs | 341 +++++++++ .../ExtensionObjectViewModel.cs | 2 +- .../IContextMenuFactory.cs | 5 + .../ItemsUpdatedEventArgs.cs | 2 +- .../ListViewModel.cs | 3 +- .../LoadingPageViewModel.cs | 1 + .../Messages/EnterDockEditModeMessage.cs | 7 + .../Messages/NavigateToPageMessage.cs | 2 +- .../Messages/PerformCommandMessage.cs | 2 + .../Messages/PinToDockMessage.cs | 7 + .../Messages/ShowHideDockMessage.cs | 7 + .../Messages/UnpinCommandItemMessage.cs | 4 +- .../Messages/WindowHiddenMessage.cs | 7 + .../NullPageViewModel.cs | 10 +- .../PageViewModel.cs | 19 +- .../Properties/Resources.Designer.cs | 62 ++ .../Properties/Resources.resx | 28 + .../Services/DockThemeSnapshot.cs | 73 ++ .../Services/IThemeService.cs | 7 + .../Settings/DockSettings.cs | 172 +++++ .../SettingsModel.cs | 17 +- .../SettingsViewModel.cs | 57 ++ .../ShellViewModel.cs | 30 +- .../TopLevelCommandManager.cs | 188 ++++- .../TopLevelViewModel.cs | 116 ++- .../cmdpal/Microsoft.CmdPal.UI/App.xaml | 10 +- .../cmdpal/Microsoft.CmdPal.UI/App.xaml.cs | 2 + .../CommandPaletteContextMenuFactory.cs | 158 +++- .../Controls/CommandBar.xaml | 6 +- .../Controls/ScrollContainer.xaml | 302 ++++++++ .../Controls/ScrollContainer.xaml.cs | 222 ++++++ .../Microsoft.CmdPal.UI/Dock/DockControl.xaml | 449 +++++++++++ .../Dock/DockControl.xaml.cs | 510 +++++++++++++ .../Dock/DockItemControl.xaml | 178 +++++ .../Dock/DockItemControl.xaml.cs | 213 ++++++ .../Dock/DockSettingsToViews.cs | 67 ++ .../Microsoft.CmdPal.UI/Dock/DockWindow.xaml | 49 ++ .../Dock/DockWindow.xaml.cs | 698 ++++++++++++++++++ .../ExtViews/ListPage.xaml.cs | 2 - .../Helpers/Icons/IconCacheProvider.cs | 5 + .../Helpers/Icons/IconServiceRegistration.cs | 4 + .../Helpers/Icons/WellKnownIconSize.cs | 1 + .../Helpers/TrayIconService.cs | 2 +- .../Microsoft.CmdPal.UI/MainWindow.xaml.cs | 102 ++- .../Microsoft.CmdPal.UI.csproj | 40 +- .../Microsoft.CmdPal.UI/NativeMethods.txt | 43 +- .../Microsoft.CmdPal.UI/Pages/ShellPage.xaml | 13 +- .../Pages/ShellPage.xaml.cs | 57 +- .../Services/ThemeService.cs | 74 +- .../Settings/DockSettingsPage.xaml | 254 +++++++ .../Settings/DockSettingsPage.xaml.cs | 218 ++++++ .../Settings/SettingsWindow.xaml | 6 + .../Settings/SettingsWindow.xaml.cs | 23 +- .../Strings/en-us/Resources.resw | 111 +++ .../Microsoft.CmdPal.UI/Styles/Colors.xaml | 8 - .../Styles/TeachingTip.xaml | 442 +++++++++++ src/modules/cmdpal/custom.props | 2 +- .../Microsoft.CmdPal.Ext.Calc.csproj | 12 +- .../ClipboardHistoryCommandsProvider.cs | 6 +- .../PerformanceMonitorCommandsProvider.cs | 9 +- .../Microsoft.CmdPal.Ext.TimeDate.csproj | 1 + .../Properties/Resources.Designer.cs | 29 +- .../Properties/Resources.resx | 12 + .../TimeDateCommandsProvider.cs | 85 +++ .../Pages/WinGetExtensionPage.cs | 1 + .../SampleButtonsDockBand.cs | 30 + .../SamplePagesExtension/SampleDockBand.cs | 22 + .../SamplePagesCommandsProvider.cs | 12 + .../SamplePagesExtension/ShowToastCommand.cs | 19 + 90 files changed, 7148 insertions(+), 193 deletions(-) create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/PinnedDockItem.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockBandSettingsViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockBandViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockWindowViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DockAppearanceSettingsViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/EnterDockEditModeMessage.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PinToDockMessage.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowHideDockMessage.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/WindowHiddenMessage.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/DockThemeSnapshot.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Settings/DockSettings.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScrollContainer.xaml create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScrollContainer.xaml.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockSettingsToViews.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/DockSettingsPage.xaml create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/DockSettingsPage.xaml.cs delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Colors.xaml create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/TeachingTip.xaml create mode 100644 src/modules/cmdpal/ext/SamplePagesExtension/SampleButtonsDockBand.cs create mode 100644 src/modules/cmdpal/ext/SamplePagesExtension/SampleDockBand.cs create mode 100644 src/modules/cmdpal/ext/SamplePagesExtension/ShowToastCommand.cs diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt index fee0314208..50a4b55e5b 100644 --- a/.github/actions/spell-check/allow/code.txt +++ b/.github/actions/spell-check/allow/code.txt @@ -315,6 +315,7 @@ xef xes PACKAGEVERSIONNUMBER APPXMANIFESTVERSION +PROGMAN # MRU lists CACHEWRITE @@ -325,6 +326,14 @@ REGSTR # Misc Win32 APIs and PInvokes INVOKEIDLIST MEMORYSTATUSEX +ABE +HTCAPTION +POSCHANGED +QUERYPOS +SETAUTOHIDEBAR +WINDOWPOS +WINEVENTPROC +WORKERW # PowerRename metadata pattern abbreviations (used in tests and regex patterns) DDDD @@ -349,3 +358,6 @@ nostdin # Performance counter keys engtype Nonpaged + +# XAML +Untargeted diff --git a/src/modules/cmdpal/.wt.json b/src/modules/cmdpal/.wt.json index 230329e876..4488d6b025 100644 --- a/src/modules/cmdpal/.wt.json +++ b/src/modules/cmdpal/.wt.json @@ -26,6 +26,11 @@ "input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd", "name": "Update template project", "description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory." + }, + { + "input": " .\\extensionsdk\\nuget\\BuildSDKHelper.ps1 -VersionOfSDK 0.0.1", + "name": "Build SDK", + "description": "Builds the SDK nuget package with the specified version." } ] } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/CoreLogger.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/CoreLogger.cs index 4540544d05..650cebec32 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Common/CoreLogger.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/CoreLogger.cs @@ -2,8 +2,6 @@ // 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; - namespace Microsoft.CmdPal.Common; public static class CoreLogger @@ -15,6 +13,8 @@ public static class CoreLogger private static ILogger? _logger; + public static ILogger? Instance => _logger; + public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) { _logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/PinnedDockItem.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/PinnedDockItem.cs new file mode 100644 index 0000000000..295ea06232 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/PinnedDockItem.cs @@ -0,0 +1,24 @@ +// 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.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Common.Helpers; + +public partial class PinnedDockItem : WrappedDockItem +{ + public override string Title => $"{base.Title} ({Properties.Resources.PinnedItemSuffix})"; + + public PinnedDockItem(ICommand command) + : base(command, command.Name) + { + } + + public PinnedDockItem(IListItem item, string id) + : base([item], id, item.Title) + { + Icon = item.Icon; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.Designer.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.Designer.cs index 2837c1a1bd..f79dbd8817 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.Designer.cs @@ -72,5 +72,14 @@ namespace Microsoft.CmdPal.Common.Properties { return ResourceManager.GetString("ErrorReport_Global_Preamble", resourceCulture); } } + + /// + /// Looks up a localized string similar to Pinned. + /// + internal static string PinnedItemSuffix { + get { + return ResourceManager.GetString("PinnedItemSuffix", resourceCulture); + } + } } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.resx b/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.resx index e2aa867ad2..0939fc052a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.resx +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.resx @@ -1,4 +1,4 @@ - + - + @@ -21,6 +22,7 @@ + @@ -29,6 +31,12 @@ 240 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScrollContainer.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScrollContainer.xaml.cs new file mode 100644 index 0000000000..730ddf3f35 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScrollContainer.xaml.cs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Windows.Foundation; +using Windows.Foundation.Collections; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class ScrollContainer : UserControl +{ + public enum ScrollContentAlignment + { + Start, + End, + } + + public ScrollContainer() + { + InitializeComponent(); + Loaded += ScrollContainer_Loaded; + } + + private void ScrollContainer_Loaded(object sender, RoutedEventArgs e) + { + UpdateOrientationState(); + UpdateLayoutState(); + } + + public object Source + { + get => (object)GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + public static readonly DependencyProperty SourceProperty = + DependencyProperty.Register(nameof(Source), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null)); + + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(ScrollContainer), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged)); + + public ScrollContentAlignment ContentAlignment + { + get => (ScrollContentAlignment)GetValue(ContentAlignmentProperty); + set => SetValue(ContentAlignmentProperty, value); + } + + public static readonly DependencyProperty ContentAlignmentProperty = + DependencyProperty.Register(nameof(ContentAlignment), typeof(ScrollContentAlignment), typeof(ScrollContainer), new PropertyMetadata(ScrollContentAlignment.Start, OnContentAlignmentChanged)); + + public object ActionButton + { + get => (object)GetValue(ActionButtonProperty); + set => SetValue(ActionButtonProperty, value); + } + + public static readonly DependencyProperty ActionButtonProperty = + DependencyProperty.Register(nameof(ActionButton), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null)); + + public Visibility ActionButtonVisibility + { + get => (Visibility)GetValue(ActionButtonVisibilityProperty); + set => SetValue(ActionButtonVisibilityProperty, value); + } + + public static readonly DependencyProperty ActionButtonVisibilityProperty = + DependencyProperty.Register(nameof(ActionButtonVisibility), typeof(Visibility), typeof(ScrollContainer), new PropertyMetadata(Visibility.Collapsed)); + + private static void OnContentAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ScrollContainer control) + { + control.UpdateLayoutState(); + control.ScrollToAlignment(); + } + } + + private void ScrollToAlignment() + { + // Reset button visibility + ScrollBackBtn.Visibility = Visibility.Collapsed; + ScrollForwardBtn.Visibility = Visibility.Collapsed; + + if (ContentAlignment == ScrollContentAlignment.End) + { + // Scroll to the end + if (Orientation == Orientation.Horizontal) + { + scroller.ChangeView(scroller.ScrollableWidth, null, null, true); + } + else + { + scroller.ChangeView(null, scroller.ScrollableHeight, null, true); + } + } + else + { + // Scroll to the beginning + scroller.ChangeView(0, 0, null, true); + } + + // Defer visibility update until after layout + void OnLayoutUpdated(object? sender, object args) + { + scroller.LayoutUpdated -= OnLayoutUpdated; + UpdateScrollButtonsVisibility(); + } + + scroller.LayoutUpdated += OnLayoutUpdated; + } + + private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ScrollContainer control) + { + control.UpdateOrientationState(); + control.UpdateLayoutState(); + control.ScrollToAlignment(); + } + } + + private void UpdateOrientationState() + { + var stateName = Orientation == Orientation.Horizontal ? "HorizontalState" : "VerticalState"; + VisualStateManager.GoToState(this, stateName, true); + } + + private void UpdateLayoutState() + { + var isHorizontal = Orientation == Orientation.Horizontal; + var isStart = ContentAlignment == ScrollContentAlignment.Start; + + var stateName = (isHorizontal, isStart) switch + { + (true, true) => "HorizontalStartState", + (true, false) => "HorizontalEndState", + (false, true) => "VerticalStartState", + (false, false) => "VerticalEndState", + }; + + VisualStateManager.GoToState(this, stateName, true); + } + + private void Scroller_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e) + { + UpdateScrollButtonsVisibility(e.FinalView.HorizontalOffset, e.FinalView.VerticalOffset); + } + + private void ScrollBackBtn_Click(object sender, RoutedEventArgs e) + { + if (Orientation == Orientation.Horizontal) + { + scroller.ChangeView(scroller.HorizontalOffset - scroller.ViewportWidth, null, null); + } + else + { + scroller.ChangeView(null, scroller.VerticalOffset - scroller.ViewportHeight, null); + } + + // Manually focus to ScrollForwardBtn since this button disappears after scrolling to the end. + ScrollForwardBtn.Focus(FocusState.Programmatic); + } + + private void ScrollForwardBtn_Click(object sender, RoutedEventArgs e) + { + if (Orientation == Orientation.Horizontal) + { + scroller.ChangeView(scroller.HorizontalOffset + scroller.ViewportWidth, null, null); + } + else + { + scroller.ChangeView(null, scroller.VerticalOffset + scroller.ViewportHeight, null); + } + + // Manually focus to ScrollBackBtn since this button disappears after scrolling to the end. + ScrollBackBtn.Focus(FocusState.Programmatic); + } + + private void Scroller_SizeChanged(object sender, SizeChangedEventArgs e) + { + UpdateScrollButtonsVisibility(); + } + + private void UpdateScrollButtonsVisibility(double? horizontalOffset = null, double? verticalOffset = null) + { + var hOffset = horizontalOffset ?? scroller.HorizontalOffset; + var vOffset = verticalOffset ?? scroller.VerticalOffset; + + if (Orientation == Orientation.Horizontal) + { + ScrollBackBtn.Visibility = hOffset > 1 ? Visibility.Visible : Visibility.Collapsed; + ScrollForwardBtn.Visibility = scroller.ScrollableWidth > 0 && hOffset < scroller.ScrollableWidth - 1 + ? Visibility.Visible + : Visibility.Collapsed; + } + else + { + ScrollBackBtn.Visibility = vOffset > 1 ? Visibility.Visible : Visibility.Collapsed; + ScrollForwardBtn.Visibility = scroller.ScrollableHeight > 0 && vOffset < scroller.ScrollableHeight - 1 + ? Visibility.Visible + : Visibility.Collapsed; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml new file mode 100644 index 0000000000..1987bb0675 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +