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 <niels.laute@live.nl>
This commit is contained in:
Mike Griese
2026-02-27 07:24:23 -06:00
committed by GitHub
parent 494c14fb88
commit 70bf430d9f
90 changed files with 7148 additions and 193 deletions

View File

@@ -22,12 +22,12 @@
</ItemGroup>
<ItemGroup>
<CsWinRTInputs Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.winmd" />
<Content Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.winmd" Link="CalculatorEngineCommon.winmd">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.winmd" Link="CalculatorEngineCommon.winmd">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -16,7 +16,8 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
public ClipboardHistoryCommandsProvider()
{
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
var page = new ClipboardHistoryListPage(_settingsManager);
_clipboardHistoryListItem = new ListItem(page)
{
Title = Properties.Resources.list_item_title,
Icon = Icons.ClipboardListIcon,
@@ -24,7 +25,6 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
new CommandContextItem(_settingsManager.Settings.SettingsPage),
],
};
DisplayName = Properties.Resources.provider_display_name;
Icon = Icons.ClipboardListIcon;
Id = "Windows.ClipboardHistory";

View File

@@ -32,9 +32,8 @@ public partial class PerformanceMonitorCommandsProvider : CommandProvider
return _commands;
}
// Soon...
// public override ICommandItem[]? GetDockBands()
// {
// return new ICommandItem[] { _band };
// }
public override ICommandItem[]? GetDockBands()
{
return new ICommandItem[] { _band };
}
}

View File

@@ -22,6 +22,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup>

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@@ -159,6 +159,15 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
}
/// <summary>
/// Looks up a localized string similar to Clock.
/// </summary>
public static string Microsoft_plugin_timedate_dock_band_title {
get {
return ResourceManager.GetString("Microsoft_plugin_timedate_dock_band_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Era.
/// </summary>
@@ -932,5 +941,23 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
return ResourceManager.GetString("Microsoft_plugin_timedate_Year", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy date.
/// </summary>
public static string timedate_copy_date_command_name {
get {
return ResourceManager.GetString("timedate_copy_date_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy time.
/// </summary>
public static string timedate_copy_time_command_name {
get {
return ResourceManager.GetString("timedate_copy_time_command_name", resourceCulture);
}
}
}
}

View File

@@ -432,4 +432,16 @@
<data name="Microsoft_plugin_timedate_SettingEnableFallbackItems_Description" xml:space="preserve">
<value>Show time and date results when typing keywords like "week", "year", "now", "time", or "date"</value>
</data>
<data name="Microsoft_plugin_timedate_dock_band_title" xml:space="preserve">
<value>Clock</value>
<comment>Title for the time and date dock band</comment>
</data>
<data name="timedate_copy_time_command_name" xml:space="preserve">
<value>Copy time</value>
<comment>Name of a command to copy the current time to the clipboard</comment>
</data>
<data name="timedate_copy_date_command_name" xml:space="preserve">
<value>Copy date</value>
<comment>Name of a command to copy the current date to the clipboard</comment>
</data>
</root>

View File

@@ -20,6 +20,8 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
private static readonly TimeDateExtensionPage _timeDateExtensionPage = new(_settingsManager);
private readonly FallbackTimeDateItem _fallbackTimeDateItem = new(_settingsManager);
private readonly ListItem _bandItem;
public TimeDateCommandsProvider()
{
DisplayName = Resources.Microsoft_plugin_timedate_plugin_name;
@@ -33,6 +35,8 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
Icon = _timeDateExtensionPage.Icon;
Settings = _settingsManager.Settings;
_bandItem = new NowDockBand();
}
private string GetTranslatedPluginDescription()
@@ -47,4 +51,85 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
public override ICommandItem[] TopLevelCommands() => [_command];
public override IFallbackCommandItem[] FallbackCommands() => [_fallbackTimeDateItem];
public override ICommandItem[] GetDockBands()
{
var wrappedBand = new WrappedDockItem(
[_bandItem],
"com.microsoft.cmdpal.timedate.dockBand",
Resources.Microsoft_plugin_timedate_dock_band_title);
return new ICommandItem[] { wrappedBand };
}
}
#pragma warning disable SA1402 // File may only contain a single type
internal sealed partial class NowDockBand : ListItem
{
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(60);
private CopyTextCommand _copyTimeCommand;
private CopyTextCommand _copyDateCommand;
public NowDockBand()
{
Command = new NoOpCommand() { Id = "com.microsoft.cmdpal.timedate.dockBand" };
_copyTimeCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_time_command_name };
_copyDateCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_date_command_name };
MoreCommands = [
new CommandContextItem(_copyTimeCommand),
new CommandContextItem(_copyDateCommand)
];
UpdateText();
// Create a timer to update the time every minute
System.Timers.Timer timer = new(UpdateInterval.TotalMilliseconds);
// but we want it to tick on the minute, so calculate the initial delay
var now = DateTime.Now;
timer.Interval = UpdateInterval.TotalMilliseconds - ((now.Second * 1000) + now.Millisecond);
// then after the first tick, set it to 60 seconds
timer.Elapsed += Timer_ElapsedFirst;
timer.Start();
}
private void Timer_ElapsedFirst(object sender, System.Timers.ElapsedEventArgs e)
{
// After the first tick, set the interval to 60 seconds
if (sender is System.Timers.Timer timer)
{
timer.Interval = UpdateInterval.TotalMilliseconds;
timer.Elapsed -= Timer_ElapsedFirst;
timer.Elapsed += Timer_Elapsed;
// Still call the callback, so that we update the clock
Timer_Elapsed(sender, e);
}
}
private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
UpdateText();
}
private void UpdateText()
{
var timeExtended = false; // timeLongFormat ?? settings.TimeWithSecond;
var dateExtended = false; // dateLongFormat ?? settings.DateWithWeekday;
var dateTimeNow = DateTime.Now;
var timeString = dateTimeNow.ToString(
TimeAndDateHelper.GetStringFormat(FormatStringType.Time, timeExtended, dateExtended),
CultureInfo.CurrentCulture);
var dateString = dateTimeNow.ToString(
TimeAndDateHelper.GetStringFormat(FormatStringType.Date, timeExtended, dateExtended),
CultureInfo.CurrentCulture);
Title = timeString;
Subtitle = dateString;
_copyDateCommand.Text = dateString;
_copyTimeCommand.Text = timeString;
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -42,6 +42,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
public WinGetExtensionPage(string tag = "")
{
Icon = tag == ExtensionsTag ? Icons.ExtensionsIcon : Icons.WinGetIcon;
Id = tag == ExtensionsTag ? "com.microsoft.cmdpal.winget-extensions" : "com.microsoft.cmdpal.winget";
Name = Properties.Resources.winget_page_name;
_tag = tag;
ShowDetails = true;

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension;
/// <summary>
/// A sample dock band with multiple buttons.
/// Each button shows a toast message when clicked.
/// </summary>
internal sealed partial class SampleButtonsDockBand : WrappedDockItem
{
public SampleButtonsDockBand()
: base([], "com.microsoft.cmdpal.samples.buttons_band", "Sample Buttons Band")
{
ListItem[] buttons = [
new(new ShowToastCommand("Button 1")) { Title = "1" },
new(new ShowToastCommand("Button B")) { Icon = new IconInfo("\uF094") }, // B button
new(new ShowToastCommand("Button 3")) { Title = "Items have Icons &", Icon = new IconInfo("\uED1E"), Subtitle = "titles & subtitles" }, // Subtitles
];
Icon = new IconInfo("\uEECA"); // ButtonView2
Items = buttons;
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,22 @@
// 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.Toolkit;
namespace SamplePagesExtension;
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// A sample dock band with one button.
/// Clicking on this button will open the palette to the samples list page
/// </summary>
internal sealed partial class SampleDockBand : WrappedDockItem
{
public SampleDockBand()
: base(new SamplesListPage(), "Command Palette Samples")
{
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -27,4 +28,15 @@ public partial class SamplePagesCommandsProvider : CommandProvider
{
return _commands;
}
public override ICommandItem[] GetDockBands()
{
List<ICommandItem> bands = new()
{
new SampleDockBand(),
new SampleButtonsDockBand(),
};
return bands.ToArray();
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension;
internal sealed partial class ShowToastCommand(string message) : InvokableCommand
{
public override ICommandResult Invoke()
{
return CommandResult.ShowToast(message);
}
}
#pragma warning restore SA1402 // File may only contain a single type