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

@@ -0,0 +1,449 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Dock.DockControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:coreVm="using:Microsoft.CmdPal.Core.ViewModels"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dockVm="using:Microsoft.CmdPal.UI.ViewModels.Dock"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:vm="using:Microsoft.CmdPal.UI.ViewModels"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<StackLayout
x:Key="ItemsOrientationLayout"
Orientation="{x:Bind ItemsOrientation, Mode=OneWay}"
Spacing="4" />
<ItemsPanelTemplate x:Key="HorizontalItemsPanel">
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="VerticalItemsPanel">
<StackPanel Orientation="Vertical" Spacing="4" />
</ItemsPanelTemplate>
<DataTemplate x:Key="DockBandTemplate" x:DataType="dockVm:DockBandViewModel">
<ItemsRepeater ItemsSource="{x:Bind Items, Mode=OneWay}" Layout="{StaticResource ItemsOrientationLayout}">
<ItemsRepeater.Transitions>
<TransitionCollection />
</ItemsRepeater.Transitions>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="dockVm:DockItemViewModel">
<local:DockItemControl
Title="{x:Bind Title, Mode=OneWay}"
RightTapped="BandItem_RightTapped"
Subtitle="{x:Bind Subtitle, Mode=OneWay}"
Tag="{x:Bind}"
Tapped="BandItem_Tapped"
ToolTip="{x:Bind Tooltip, Mode=OneWay}">
<local:DockItemControl.Icon>
<cpcontrols:IconBox
x:Name="IconBorder"
Width="16"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested16}" />
</local:DockItemControl.Icon>
</local:DockItemControl>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</DataTemplate>
<Style x:Key="DockBandListViewStyle" TargetType="ListView">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Padding" Value="0" />
<Setter Property="IsItemClickEnabled" Value="False" />
<Setter Property="SelectionMode" Value="None" />
<!-- Drag properties controlled by code-behind based on IsEditMode -->
<Setter Property="CanDragItems" Value="False" />
<Setter Property="CanReorderItems" Value="False" />
<Setter Property="AllowDrop" Value="False" />
</Style>
<Style x:Key="DockBandListViewItemStyle" TargetType="ListViewItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0,0,4,0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
</Style>
<Style
x:Name="ContextMenuFlyoutStyle"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Style.Setters>
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
</Style.Setters>
</Style>
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
<Flyout
x:Name="ContextMenuFlyout"
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
Opened="ContextMenuFlyout_Opened"
ShouldConstrainToRootBounds="False"
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
<cpcontrols:ContextMenu x:Name="ContextControl" />
</Flyout>
<!-- Edit mode context menu for dock bands -->
<MenuFlyout x:Name="EditModeContextMenu" ShouldConstrainToRootBounds="False">
<MenuFlyoutSubItem x:Name="LabelsSubMenu" Text="Labels">
<MenuFlyoutSubItem.Icon>
<FontIcon Glyph="&#xE8EC;" />
</MenuFlyoutSubItem.Icon>
<ToggleMenuFlyoutItem
x:Name="ShowTitlesMenuItem"
Click="ShowTitlesMenuItem_Click"
Text="Show titles" />
<ToggleMenuFlyoutItem
x:Name="ShowSubtitlesMenuItem"
Click="ShowSubtitlesMenuItem_Click"
Text="Show subtitles" />
</MenuFlyoutSubItem>
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Name="UnpinBandMenuItem"
Click="UnpinBandMenuItem_Click"
Text="Unpin">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE77A;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
</MenuFlyout>
<!-- Add band flyout - used in edit mode to add bands to dock sections -->
<Flyout
x:Name="AddBandFlyout"
Placement="Bottom"
ShouldConstrainToRootBounds="False">
<StackPanel Width="320">
<TextBlock
x:Name="NoAvailableBandsText"
Padding="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="No commands available to pin"
TextAlignment="Center"
Visibility="Collapsed" />
<ListView
x:Name="AddBandListView"
MaxHeight="300"
HorizontalAlignment="Stretch"
IsItemClickEnabled="True"
ItemClick="AddBandListView_ItemClick"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:TopLevelViewModel">
<Grid Padding="4" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<cpcontrols:IconBox
Width="20"
Height="20"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind IconViewModel, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Bind Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Flyout>
</ResourceDictionary>
</UserControl.Resources>
<Grid
x:Name="RootGrid"
BorderThickness="0,0,0,1"
RightTapped="RootGrid_RightTapped">
<!-- Edit Mode Overlay - shown when in edit mode -->
<Grid
x:Name="ContentGrid"
Margin="4"
Padding="4,0,4,0"
Background="Transparent"
RightTapped="RootGrid_RightTapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<cpcontrols:ScrollContainer
x:Name="StartScroller"
Grid.Row="0"
Grid.RowSpan="3"
HorizontalAlignment="Left"
VerticalAlignment="Stretch">
<cpcontrols:ScrollContainer.ActionButton>
<Button
x:Name="StartAddButton"
Click="AddBandButton_Click"
Style="{StaticResource SubtleButtonStyle}"
Tag="Start"
ToolTipService.ToolTip="Add band to Start">
<FontIcon FontSize="12" Glyph="&#xE710;" />
</Button>
</cpcontrols:ScrollContainer.ActionButton>
<cpcontrols:ScrollContainer.Source>
<ListView
x:Name="StartListView"
HorizontalAlignment="Stretch"
DragItemsCompleted="BandListView_DragItemsCompleted"
DragItemsStarting="BandListView_DragItemsStarting"
DragOver="BandListView_DragOver"
Drop="StartListView_Drop"
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
ItemTemplate="{StaticResource DockBandTemplate}"
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{x:Bind ViewModel.StartItems, Mode=OneWay}"
SelectionMode="None"
Style="{StaticResource DockBandListViewStyle}" />
</cpcontrols:ScrollContainer.Source>
</cpcontrols:ScrollContainer>
<cpcontrols:ScrollContainer
x:Name="CenterScroller"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
MinWidth="48"
HorizontalAlignment="Center"
VerticalAlignment="Stretch">
<cpcontrols:ScrollContainer.ActionButton>
<Button
x:Name="CenterAddButton"
Click="AddBandButton_Click"
Style="{StaticResource SubtleButtonStyle}"
Tag="Center"
ToolTipService.ToolTip="Add band to Center">
<FontIcon FontSize="12" Glyph="&#xE710;" />
</Button>
</cpcontrols:ScrollContainer.ActionButton>
<cpcontrols:ScrollContainer.Source>
<ListView
x:Name="CenterListView"
HorizontalAlignment="Stretch"
DragItemsCompleted="BandListView_DragItemsCompleted"
DragItemsStarting="BandListView_DragItemsStarting"
DragOver="BandListView_DragOver"
Drop="CenterListView_Drop"
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
ItemTemplate="{StaticResource DockBandTemplate}"
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{x:Bind ViewModel.CenterItems, Mode=OneWay}"
SelectionMode="None"
Style="{StaticResource DockBandListViewStyle}" />
</cpcontrols:ScrollContainer.Source>
</cpcontrols:ScrollContainer>
<cpcontrols:ScrollContainer
x:Name="EndScroller"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
ContentAlignment="End">
<cpcontrols:ScrollContainer.ActionButton>
<Button
x:Name="EndAddButton"
Click="AddBandButton_Click"
Style="{StaticResource SubtleButtonStyle}"
Tag="End"
ToolTipService.ToolTip="Add band to End">
<FontIcon FontSize="12" Glyph="&#xE710;" />
</Button>
</cpcontrols:ScrollContainer.ActionButton>
<cpcontrols:ScrollContainer.Source>
<ListView
x:Name="EndListView"
DragItemsCompleted="BandListView_DragItemsCompleted"
DragItemsStarting="BandListView_DragItemsStarting"
DragOver="BandListView_DragOver"
Drop="EndListView_Drop"
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
ItemTemplate="{StaticResource DockBandTemplate}"
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{x:Bind ViewModel.EndItems, Mode=OneWay}"
SelectionMode="None"
Style="{StaticResource DockBandListViewStyle}">
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
</ListView>
</cpcontrols:ScrollContainer.Source>
</cpcontrols:ScrollContainer>
<TeachingTip
x:Name="EditButtonsTeachingTip"
MinWidth="0"
PreferredPlacement="Bottom"
ShouldConstrainToRootBounds="False"
Style="{StaticResource TeachingTipWithoutCloseButtonStyle}"
Target="{x:Bind ContentGrid}">
<TeachingTip.Content>
<StackPanel
x:Name="EditButtonsPanel"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="4">
<Button
HorizontalAlignment="Stretch"
Click="DoneEditingButton_Click"
Content="Save"
Style="{StaticResource AccentButtonStyle}" />
<Button
HorizontalAlignment="Stretch"
Click="DiscardEditingButton_Click"
Content="Discard" />
</StackPanel>
</TeachingTip.Content>
</TeachingTip>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DockOrientation">
<VisualState x:Name="DockOnTop">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Top" />
</VisualState.StateTriggers>
</VisualState>
<VisualState x:Name="DockOnBottom">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Bottom" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="RootGrid.BorderThickness" Value="0,1,0,0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DockOnLeft">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Left" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="StartScroller.(Grid.Row)" Value="0" />
<Setter Target="StartScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="StartScroller.(Grid.Column)" Value="0" />
<Setter Target="StartScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="StartScroller.Orientation" Value="Vertical" />
<Setter Target="StartScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="StartScroller.VerticalAlignment" Value="Top" />
<Setter Target="StartScroller.Orientation" Value="Vertical" />
<Setter Target="CenterScroller.(Grid.Row)" Value="1" />
<Setter Target="CenterScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="CenterScroller.(Grid.Column)" Value="0" />
<Setter Target="CenterScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
<Setter Target="CenterScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="CenterScroller.VerticalAlignment" Value="Center" />
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
<Setter Target="EndScroller.Orientation" Value="Vertical" />
<Setter Target="EndScroller.(Grid.Row)" Value="2" />
<Setter Target="EndScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="EndScroller.(Grid.Column)" Value="0" />
<Setter Target="EndScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="EndScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="EndScroller.VerticalAlignment" Value="Bottom" />
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
<Setter Target="RootGrid.BorderThickness" Value="0,0,1,0" />
<Setter Target="StartListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
<Setter Target="CenterListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
<Setter Target="EndListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DockOnRight">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Right" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="StartScroller.(Grid.Row)" Value="0" />
<Setter Target="StartScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="StartScroller.(Grid.Column)" Value="0" />
<Setter Target="StartScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="StartScroller.Orientation" Value="Vertical" />
<Setter Target="StartScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="StartScroller.VerticalAlignment" Value="Top" />
<Setter Target="StartScroller.Orientation" Value="Vertical" />
<Setter Target="CenterScroller.(Grid.Row)" Value="1" />
<Setter Target="CenterScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="CenterScroller.(Grid.Column)" Value="0" />
<Setter Target="CenterScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
<Setter Target="CenterScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="CenterScroller.VerticalAlignment" Value="Center" />
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
<Setter Target="EndScroller.Orientation" Value="Vertical" />
<Setter Target="EndScroller.(Grid.Row)" Value="2" />
<Setter Target="EndScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="EndScroller.(Grid.Column)" Value="0" />
<Setter Target="EndScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="EndScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="EndScroller.VerticalAlignment" Value="Bottom" />
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
<Setter Target="RootGrid.BorderThickness" Value="1,0,0,0" />
<Setter Target="StartListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
<Setter Target="CenterListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
<Setter Target="EndListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<!-- Edit Mode Visual States -->
<VisualStateGroup x:Name="EditModeStates">
<VisualState x:Name="EditModeOff" />
<VisualState x:Name="EditModeOn">
<VisualState.Setters>
<Setter Target="StartScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Target="StartScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Target="StartScroller.BorderThickness" Value="1" />
<Setter Target="StartScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
<Setter Target="CenterScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Target="CenterScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Target="CenterScroller.BorderThickness" Value="1" />
<Setter Target="CenterScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
<Setter Target="EndScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Target="EndScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Target="EndScroller.BorderThickness" Value="1" />
<Setter Target="EndScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
<Setter Target="StartScroller.ActionButtonVisibility" Value="Visible" />
<Setter Target="CenterScroller.ActionButtonVisibility" Value="Visible" />
<Setter Target="EndScroller.ActionButtonVisibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>