Merge branch 'niels9001/cmdpal-dock/drag' into dev/migrie/f/powerdock

This commit is contained in:
Mike Griese
2026-01-23 06:12:49 -06:00
14 changed files with 929 additions and 134 deletions

View File

@@ -19,6 +19,7 @@ public sealed partial class DockViewModel : IDisposable,
IPageContext
{
private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly SettingsModel _settingsModel;
private DockSettings _settings;
@@ -36,6 +37,7 @@ public sealed partial class DockViewModel : IDisposable,
TaskScheduler scheduler)
{
_topLevelCommandManager = tlcManager;
_settingsModel = settings;
_settings = settings.DockSettings;
Scheduler = scheduler;
WeakReferenceMessenger.Default.Register<CommandsReloadedMessage>(this);
@@ -135,6 +137,176 @@ public sealed partial class DockViewModel : IDisposable,
return null;
}
/// <summary>
/// Syncs the band position in settings after a same-list reorder.
/// Does not save to disk - call SaveBandOrder() when done editing.
/// </summary>
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
{
var bandId = band.Id;
var dockSettings = _settingsModel.DockSettings;
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.Id == bandId)
?? dockSettings.EndBands.FirstOrDefault(b => b.Id == bandId);
if (bandSettings == null)
{
return;
}
// Remove from both settings lists
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
// Add to target settings list at the correct index
var targetSettings = targetSide == DockPinSide.Start ? dockSettings.StartBands : dockSettings.EndBands;
var insertIndex = Math.Min(targetIndex, targetSettings.Count);
targetSettings.Insert(insertIndex, bandSettings);
}
/// <summary>
/// Moves a dock band to a new position (cross-list drop).
/// Does not save to disk - call SaveBandOrder() when done editing.
/// </summary>
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
{
var bandId = band.Id;
var dockSettings = _settingsModel.DockSettings;
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.Id == bandId)
?? dockSettings.EndBands.FirstOrDefault(b => b.Id == bandId);
if (bandSettings == null)
{
Logger.LogWarning($"Could not find band settings for band {bandId}");
return;
}
// Remove from both sides (settings and UI)
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
StartItems.Remove(band);
EndItems.Remove(band);
// Add to the target side at the specified index
switch (targetSide)
{
case DockPinSide.Start:
{
var settingsIndex = Math.Min(targetIndex, dockSettings.StartBands.Count);
dockSettings.StartBands.Insert(settingsIndex, bandSettings);
var uiIndex = Math.Min(targetIndex, StartItems.Count);
StartItems.Insert(uiIndex, band);
break;
}
case DockPinSide.End:
{
var settingsIndex = Math.Min(targetIndex, dockSettings.EndBands.Count);
dockSettings.EndBands.Insert(settingsIndex, bandSettings);
var uiIndex = Math.Min(targetIndex, EndItems.Count);
EndItems.Insert(uiIndex, band);
break;
}
}
Logger.LogDebug($"Moved band {bandId} to {targetSide} at index {targetIndex} (not saved yet)");
}
/// <summary>
/// Saves the current band order to settings.
/// Call this when exiting edit mode.
/// </summary>
public void SaveBandOrder()
{
_snapshotStartBands = null;
_snapshotEndBands = null;
SettingsModel.SaveSettings(_settingsModel);
Logger.LogDebug("Saved band order to settings");
}
private List<DockBandSettings>? _snapshotStartBands;
private List<DockBandSettings>? _snapshotEndBands;
/// <summary>
/// Takes a snapshot of the current band order before editing.
/// Call this when entering edit mode.
/// </summary>
public void SnapshotBandOrder()
{
var dockSettings = _settingsModel.DockSettings;
_snapshotStartBands = dockSettings.StartBands.Select(b => new DockBandSettings { Id = b.Id }).ToList();
_snapshotEndBands = dockSettings.EndBands.Select(b => new DockBandSettings { Id = b.Id }).ToList();
Logger.LogDebug($"Snapshot taken: {_snapshotStartBands.Count} start bands, {_snapshotEndBands.Count} end bands");
}
/// <summary>
/// Restores the band order from the snapshot taken when entering edit mode.
/// Call this when discarding edit mode changes.
/// </summary>
public void RestoreBandOrder()
{
if (_snapshotStartBands == null || _snapshotEndBands == null)
{
Logger.LogWarning("No snapshot to restore from");
return;
}
var dockSettings = _settingsModel.DockSettings;
// Restore settings from snapshot
dockSettings.StartBands.Clear();
dockSettings.EndBands.Clear();
foreach (var bandSnapshot in _snapshotStartBands)
{
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id };
dockSettings.StartBands.Add(bandSettings);
}
foreach (var bandSnapshot in _snapshotEndBands)
{
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id };
dockSettings.EndBands.Add(bandSettings);
}
// Rebuild UI collections from restored settings
RebuildUICollections();
_snapshotStartBands = null;
_snapshotEndBands = null;
Logger.LogDebug("Restored band order from snapshot");
}
private void RebuildUICollections()
{
var dockSettings = _settingsModel.DockSettings;
// Create a lookup of all current band ViewModels
var allBands = StartItems.Concat(EndItems).ToDictionary(b => b.Id);
StartItems.Clear();
EndItems.Clear();
foreach (var bandSettings in dockSettings.StartBands)
{
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
{
StartItems.Add(bandVM);
}
}
foreach (var bandSettings in dockSettings.EndBands)
{
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
{
EndItems.Add(bandVM);
}
}
}
public void ShowException(Exception ex, string? extensionHint = null)
{
var extensionText = extensionHint ?? "<unknown>";
@@ -162,18 +334,29 @@ public sealed partial class DockViewModel : IDisposable,
{
public DockContextMenuItem()
{
var editDockCommand = new AnonymousCommand(
action: () =>
{
WeakReferenceMessenger.Default.Send(new EnterDockEditModeMessage());
})
{
Name = "Edit dock", // TODO!Loc
Icon = Icons.EditIcon,
};
var openSettingsCommand = new AnonymousCommand(
action: () =>
{
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Dock"));
})
{
Name = "Customize", // TODO!Loc
Name = "Dock settings", // TODO!Loc
Icon = Icons.SettingsIcon,
};
MoreCommands = new CommandContextItem[]
{
new CommandContextItem(editDockCommand),
new CommandContextItem(openSettingsCommand),
};
}

View File

@@ -13,4 +13,6 @@ internal sealed class Icons
internal static IconInfo UnpinIcon => new("\uE77A"); // Unpin icon
internal static IconInfo SettingsIcon => new("\uE713"); // Settings icon
internal static IconInfo EditIcon => new("\uE70F"); // Edit icon
}

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.CmdPal.UI.ViewModels.Messages;
public record EnterDockEditModeMessage();

View File

@@ -20,6 +20,7 @@
<ResourceDictionary Source="ms-appx:///Controls/Tag.xaml" />
<ResourceDictionary Source="ms-appx:///Controls/KeyVisual/KeyVisual.xaml" />
<ResourceDictionary Source="ms-appx:///Controls/IsEnabledTextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///Dock/DockItemControl.xaml" />
<!-- Default theme dictionary -->
<ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" />
<services:MutableOverridesDictionary />

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;
using System.Collections.ObjectModel;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.UI.Xaml;
namespace Microsoft.CmdPal.UI.Dock;
internal sealed partial class BandAlignmentConverter : Microsoft.UI.Xaml.Data.IValueConverter
{
public DockControl? Control { get; set; }
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is ObservableCollection<DockItemViewModel> items && Control is not null)
{
return Control.GetBandAlignment(items);
}
return HorizontalAlignment.Center;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,29 @@
// 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;
namespace Microsoft.CmdPal.UI.Dock;
internal sealed partial class DockBandTemplateSelector : DataTemplateSelector
{
public DockControl? Control { get; set; }
public DataTemplate? HorizontalTemplate { get; set; }
public DataTemplate? VerticalTemplate { get; set; }
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
{
if (Control is null)
{
return HorizontalTemplate;
}
return Control.ItemsOrientation == Orientation.Horizontal
? HorizontalTemplate
: VerticalTemplate;
}
}

View File

@@ -16,10 +16,12 @@
<UserControl.Resources>
<ResourceDictionary>
<StackLayout
x:Key="ItemsOrientation"
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>
<Style x:Key="ResizingIconStyle" TargetType="cpcontrols:IconBox">
<Setter Property="Height" Value="{x:Bind IconSize, Mode=OneWay}" />
@@ -32,88 +34,75 @@
</Style>
<local:IconInfoVisibilityConverter x:Key="IconInfoVisibilityConverter" />
<local:BandAlignmentConverter
x:Key="BandAlignmentConverter"
x:Name="BandAlignmentConverter"
Control="{x:Bind}" />
<local:BandAlignmentConverter x:Key="BandAlignmentConverter" Control="{x:Bind}" />
<DataTemplate x:Key="DeskbandTemplate" x:DataType="dockVm:DockItemViewModel">
<Button
VerticalAlignment="Stretch"
DataContext="{x:Bind}"
RightTapped="BandItem_RightTapped"
Style="{StaticResource TaskBarButtonStyle}"
Tapped="BandItem_Tapped"
ToolTipService.ToolTip="{x:Bind Tooltip, Mode=OneWay}">
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
VerticalAlignment="Center"
Orientation="Vertical"
Visibility="{x:Bind Icon, Converter={StaticResource IconInfoVisibilityConverter}, Mode=OneWay}">
<Grid>
<TextBlock Opacity="0" Style="{StaticResource ResizingTitleTextBlock}" />
<!-- Removing this breaks the app.. and I have no idea why -->
<local:DockItemControl
Title="{x:Bind Title, Mode=OneWay}"
RightTapped="BandItem_RightTapped"
Subtitle="{x:Bind Subtitle, Mode=OneWay}"
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.SourceRequested}"
Style="{StaticResource ResizingIconStyle}" />
</StackPanel>
<StackPanel
Grid.Column="1"
Margin="8,0,8,0"
VerticalAlignment="Center"
Visibility="{x:Bind HasText, Mode=OneWay}">
<TextBlock
x:Name="TitleTextBlock"
Margin="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Style="{StaticResource ResizingTitleTextBlock}"
Text="{x:Bind Title, Mode=OneWay}"
TextAlignment="Left"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
x:Name="SubTitleTextBlock"
MaxWidth="100"
Margin="0,-4,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="10"
Foreground="{ThemeResource TextFillColorTertiary}"
Text="{x:Bind Subtitle, Mode=OneWay}"
TextAlignment="Center"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringNotEmptyToVisibilityConverter}}" />
</StackPanel>
</Grid>
</Button>
</local:DockItemControl.Icon>
</local:DockItemControl>
</Grid>
</DataTemplate>
<DataTemplate x:Key="DockBandTemplate" x:DataType="dockVm:DockBandViewModel">
<ItemsRepeater
x:Name="BandItemsRepeater"
<DataTemplate x:Key="HorizontalDockBandTemplate" x:DataType="dockVm:DockBandViewModel">
<ItemsControl
HorizontalAlignment="{x:Bind Items, Converter={StaticResource BandAlignmentConverter}, Mode=OneWay}"
ItemTemplate="{StaticResource DeskbandTemplate}"
ItemsSource="{x:Bind Items, Mode=OneWay}"
Layout="{StaticResource ItemsOrientation}">
<ItemsRepeater.Transitions>
<TransitionCollection />
</ItemsRepeater.Transitions>
</ItemsRepeater>
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{x:Bind Items, Mode=OneWay}" />
</DataTemplate>
<DataTemplate x:Key="VerticalDockBandTemplate" x:DataType="dockVm:DockBandViewModel">
<ItemsControl
HorizontalAlignment="{x:Bind Items, Converter={StaticResource BandAlignmentConverter}, Mode=OneWay}"
ItemTemplate="{StaticResource DeskbandTemplate}"
ItemsPanel="{StaticResource VerticalItemsPanel}"
ItemsSource="{x:Bind Items, Mode=OneWay}" />
</DataTemplate>
<local:DockBandTemplateSelector
x:Key="DockBandTemplateSelector"
Control="{x:Bind}"
HorizontalTemplate="{StaticResource HorizontalDockBandTemplate}"
VerticalTemplate="{StaticResource VerticalDockBandTemplate}" />
<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}"
@@ -143,10 +132,18 @@
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
RightTapped="RootGrid_RightTapped">
<!-- Edit Mode Overlay - shown when in edit mode -->
<Border
x:Name="EditModeOverlay"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
IsHitTestVisible="False"
Opacity="0" />
<Grid x:Name="ContentGrid" Padding="0,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition x:Name="EndColumn" Width="Auto" />
<ColumnDefinition x:Name="DoneButtonColumn" Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
@@ -159,16 +156,19 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<cpcontrols:ScrollContainer.Source>
<ItemsRepeater
x:Name="StartItemsRepeater"
<ListView
x:Name="StartItemsListView"
HorizontalAlignment="Stretch"
ItemTemplate="{StaticResource DockBandTemplate}"
DragItemsCompleted="BandListView_DragItemsCompleted"
DragItemsStarting="BandListView_DragItemsStarting"
DragOver="BandListView_DragOver"
Drop="StartItemsListView_Drop"
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
ItemTemplateSelector="{StaticResource DockBandTemplateSelector}"
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{x:Bind ViewModel.StartItems, Mode=OneWay}"
Layout="{StaticResource ItemsOrientation}">
<ItemsRepeater.Transitions>
<TransitionCollection />
</ItemsRepeater.Transitions>
</ItemsRepeater>
SelectionMode="None"
Style="{StaticResource DockBandListViewStyle}" />
</cpcontrols:ScrollContainer.Source>
</cpcontrols:ScrollContainer>
@@ -177,17 +177,46 @@
Grid.Column="1"
ContentAlignment="End">
<cpcontrols:ScrollContainer.Source>
<ItemsRepeater
x:Name="EndItemsRepeater"
ItemTemplate="{StaticResource DockBandTemplate}"
<ListView
x:Name="EndItemsListView"
DragItemsCompleted="BandListView_DragItemsCompleted"
DragItemsStarting="BandListView_DragItemsStarting"
DragOver="BandListView_DragOver"
Drop="EndItemsListView_Drop"
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
ItemTemplateSelector="{StaticResource DockBandTemplateSelector}"
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{x:Bind ViewModel.EndItems, Mode=OneWay}"
Layout="{StaticResource ItemsOrientation}">
<ItemsRepeater.Transitions>
SelectionMode="None"
Style="{StaticResource DockBandListViewStyle}">
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ItemsRepeater.Transitions>
</ItemsRepeater>
</ListView.ItemContainerTransitions>
</ListView>
</cpcontrols:ScrollContainer.Source>
</cpcontrols:ScrollContainer>
<!-- To do: remove the X button -->
<TeachingTip
x:Name="EditButtonsTeachingTip"
MinWidth="0"
PreferredPlacement="Bottom"
ShouldConstrainToRootBounds="False"
Target="{x:Bind ContentGrid}">
<TeachingTip.Content>
<StackPanel
x:Name="EditButtonsPanel"
HorizontalAlignment="Center"
Orientation="Vertical"
Spacing="4">
<Button
Click="DoneEditingButton_Click"
Content="Save"
Style="{StaticResource AccentButtonStyle}" />
<Button Click="DiscardEditingButton_Click" Content="Discard" />
</StackPanel>
</TeachingTip.Content>
</TeachingTip>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DockOrientation">
@@ -268,6 +297,16 @@
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<!-- Edit Mode Visual States -->
<VisualStateGroup x:Name="EditModeStates">
<VisualState x:Name="EditModeOff" />
<VisualState x:Name="EditModeOn">
<VisualState.Setters>
<Setter Target="EditModeOverlay.Opacity" Value="0.4" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.UI;
@@ -16,13 +17,12 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Media;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.Dock;
#pragma warning disable SA1402 // File may only contain a single type
public sealed partial class DockControl : UserControl, INotifyPropertyChanged, IRecipient<CloseContextMenuMessage>
public sealed partial class DockControl : UserControl, INotifyPropertyChanged, IRecipient<CloseContextMenuMessage>, IRecipient<EnterDockEditModeMessage>
{
private DockViewModel _viewModel;
@@ -39,10 +39,31 @@ public sealed partial class DockControl : UserControl, INotifyPropertyChanged, I
{
field = value;
PropertyChanged?.Invoke(this, new(nameof(ItemsOrientation)));
UpdateBandTemplates();
}
}
}
private void UpdateBandTemplates()
{
var panelKey = ItemsOrientation == Orientation.Horizontal
? "HorizontalItemsPanel"
: "VerticalItemsPanel";
var panel = (ItemsPanelTemplate)Resources[panelKey];
StartItemsListView.ItemsPanel = panel;
EndItemsListView.ItemsPanel = panel;
// Force the selector to re-evaluate by refreshing ItemsSource
var startItems = StartItemsListView.ItemsSource;
var endItems = EndItemsListView.ItemsSource;
StartItemsListView.ItemsSource = null;
EndItemsListView.ItemsSource = null;
StartItemsListView.ItemsSource = startItems;
EndItemsListView.ItemsSource = endItems;
}
public DockSide DockSide
{
get => field;
@@ -56,6 +77,20 @@ public sealed partial class DockControl : UserControl, INotifyPropertyChanged, I
}
}
public bool IsEditMode
{
get => field;
set
{
if (field != value)
{
field = value;
PropertyChanged?.Invoke(this, new(nameof(IsEditMode)));
UpdateEditMode(value);
}
}
}
public double IconSize
{
get => field;
@@ -109,6 +144,81 @@ public sealed partial class DockControl : UserControl, INotifyPropertyChanged, I
_viewModel = viewModel;
InitializeComponent();
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<EnterDockEditModeMessage>(this);
// Start with edit mode disabled - normal click behavior
UpdateEditMode(false);
}
public void Receive(EnterDockEditModeMessage message)
{
// Message may arrive from a background thread, dispatch to UI thread
DispatcherQueue.TryEnqueue(() =>
{
EnterEditMode();
});
}
private void UpdateEditMode(bool isEditMode)
{
// Enable/disable drag-and-drop based on edit mode
StartItemsListView.CanDragItems = isEditMode;
StartItemsListView.CanReorderItems = isEditMode;
StartItemsListView.AllowDrop = isEditMode;
EndItemsListView.CanDragItems = isEditMode;
EndItemsListView.CanReorderItems = isEditMode;
EndItemsListView.AllowDrop = isEditMode;
if (isEditMode)
{
EditButtonsTeachingTip.PreferredPlacement = DockSide switch
{
DockSide.Left => TeachingTipPlacementMode.Right,
DockSide.Right => TeachingTipPlacementMode.Left,
DockSide.Top => TeachingTipPlacementMode.Bottom,
DockSide.Bottom => TeachingTipPlacementMode.Top,
_ => TeachingTipPlacementMode.Auto,
};
}
EditButtonsTeachingTip.IsOpen = isEditMode;
// Update visual state
VisualStateManager.GoToState(this, isEditMode ? "EditModeOn" : "EditModeOff", true);
}
internal void EnterEditMode()
{
// Snapshot current state so we can restore on discard
ViewModel.SnapshotBandOrder();
IsEditMode = true;
}
internal void ExitEditMode()
{
IsEditMode = false;
// Save all changes when exiting edit mode
ViewModel.SaveBandOrder();
}
internal void DiscardEditMode()
{
IsEditMode = false;
// Restore the original band order from snapshot
ViewModel.RestoreBandOrder();
}
private void DoneEditingButton_Click(object sender, RoutedEventArgs e)
{
ExitEditMode();
}
private void DiscardEditingButton_Click(object sender, RoutedEventArgs e)
{
DiscardEditMode();
}
internal void UpdateSettings(DockSettings settings)
@@ -127,52 +237,58 @@ public sealed partial class DockControl : UserControl, INotifyPropertyChanged, I
{
RootGrid.BorderBrush = new SolidColorBrush(Colors.Transparent);
}
// Ensure templates are updated on initial load (setter only updates on change)
UpdateBandTemplates();
}
private void BandItem_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
var pos = e.GetPosition(null);
var button = sender as Button;
var item = button?.DataContext as DockItemViewModel;
if (item is not null)
// Ignore clicks when in edit mode - allow drag behavior instead
if (IsEditMode)
{
// Use the center of the button as the point to open at. This is
// more reliable than using the tap position. This allows multiple
// clicks anywhere in the button to open the palette in a consistent
// location.
var buttonPos = button!.TransformToVisual(null).TransformPoint(new Point(0, 0));
var buttonCenter = new Point(
buttonPos.X + (button.ActualWidth / 2),
buttonPos.Y + (button.ActualHeight / 2));
return;
}
InvokeItem(item, buttonCenter);
if (sender is DockItemControl dockItem && dockItem.DataContext is DockItemViewModel item)
{
// Use the center of the border as the point to open at
var borderPos = dockItem.TransformToVisual(null).TransformPoint(new Point(0, 0));
var borderCenter = new Point(
borderPos.X + (dockItem.ActualWidth / 2),
borderPos.Y + (dockItem.ActualHeight / 2));
InvokeItem(item, borderCenter);
e.Handled = true;
}
}
private void BandItem_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
{
var pos = e.GetPosition(null);
var button = sender as Button;
var item = button?.DataContext as DockItemViewModel;
if (item is not null)
// Ignore right-clicks when in edit mode
if (IsEditMode)
{
return;
}
if (sender is DockItemControl dockItem && dockItem.DataContext is DockItemViewModel item)
{
if (item.HasMoreCommands)
{
ContextControl.ViewModel.SelectedItem = item;
ContextMenuFlyout.ShowAt(
button,
new FlyoutShowOptions()
{
ShowMode = FlyoutShowMode.Standard,
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
});
dockItem,
new FlyoutShowOptions()
{
ShowMode = FlyoutShowMode.Standard,
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
});
e.Handled = true;
}
}
}
private void InvokeItem(DockItemViewModel item, global::Windows.Foundation.Point pos)
private void InvokeItem(DockItemViewModel item, Point pos)
{
var command = item.Command;
try
@@ -236,7 +352,7 @@ public sealed partial class DockControl : UserControl, INotifyPropertyChanged, I
}
var requestedTheme = ActualTheme;
var isLight = requestedTheme == Microsoft.UI.Xaml.ElementTheme.Light;
var isLight = requestedTheme == ElementTheme.Light;
// Check if any of the items have both an icon and a label.
//
@@ -256,26 +372,118 @@ public sealed partial class DockControl : UserControl, INotifyPropertyChanged, I
return HorizontalAlignment.Center;
}
}
internal sealed partial class BandAlignmentConverter : Microsoft.UI.Xaml.Data.IValueConverter
{
public DockControl? Control { get; set; }
private DockBandViewModel? _draggedBand;
public object Convert(object value, Type targetType, object parameter, string language)
private void BandListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
if (value is ObservableCollection<DockItemViewModel> items && Control is not null)
if (e.Items.Count > 0 && e.Items[0] is DockBandViewModel band)
{
return Control.GetBandAlignment(items);
_draggedBand = band;
e.Data.RequestedOperation = DataPackageOperation.Move;
}
}
private void BandListView_DragOver(object sender, DragEventArgs e)
{
if (_draggedBand != null)
{
e.AcceptedOperation = DataPackageOperation.Move;
}
}
private void BandListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
{
// Reordering within the same list is handled automatically by ListView
// We just need to sync the ViewModel order without saving
if (args.DropResult == DataPackageOperation.Move && _draggedBand != null)
{
var isStartList = sender == StartItemsListView;
var targetSide = isStartList ? DockPinSide.Start : DockPinSide.End;
var targetCollection = isStartList ? ViewModel.StartItems : ViewModel.EndItems;
// Find the new index and sync ViewModel (without saving)
var newIndex = targetCollection.IndexOf(_draggedBand);
if (newIndex >= 0)
{
ViewModel.SyncBandPosition(_draggedBand, targetSide, newIndex);
}
}
return HorizontalAlignment.Center;
_draggedBand = null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
private void StartItemsListView_Drop(object sender, DragEventArgs e)
{
throw new NotImplementedException();
HandleCrossListDrop(DockPinSide.Start, e);
}
private void EndItemsListView_Drop(object sender, DragEventArgs e)
{
HandleCrossListDrop(DockPinSide.End, e);
}
private void HandleCrossListDrop(DockPinSide targetSide, DragEventArgs e)
{
if (_draggedBand == null)
{
return;
}
// Check if this is a cross-list drop (dragging from the other list)
var isInStart = ViewModel.StartItems.Contains(_draggedBand);
var isInEnd = ViewModel.EndItems.Contains(_draggedBand);
var sourceIsStart = isInStart;
var targetIsStart = targetSide == DockPinSide.Start;
// Only handle cross-list drops here; same-list reorders are handled in DragItemsCompleted
if (sourceIsStart != targetIsStart)
{
// Calculate drop index based on drop position
var targetListView = targetIsStart ? StartItemsListView : EndItemsListView;
var targetCollection = targetIsStart ? ViewModel.StartItems : ViewModel.EndItems;
var dropIndex = GetDropIndex(targetListView, e, targetCollection.Count);
// Move the band to the new side (without saving - save happens on Done)
ViewModel.MoveBandWithoutSaving(_draggedBand, targetSide, dropIndex);
e.Handled = true;
}
}
private int GetDropIndex(ListView listView, DragEventArgs e, int itemCount)
{
var position = e.GetPosition(listView);
// Find the item at the drop position
for (var i = 0; i < itemCount; i++)
{
if (listView.ContainerFromIndex(i) is ListViewItem container)
{
var itemBounds = container.TransformToVisual(listView).TransformBounds(
new Rect(0, 0, container.ActualWidth, container.ActualHeight));
if (ItemsOrientation == Orientation.Horizontal)
{
// For horizontal layout, check X position
if (position.X < itemBounds.X + (itemBounds.Width / 2))
{
return i;
}
}
else
{
// For vertical layout, check Y position
if (position.Y < itemBounds.Y + (itemBounds.Height / 2))
{
return i;
}
}
}
}
// If we're past all items, insert at the end
return itemCount;
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.CmdPal.UI.Dock">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="#0FFFFFFF" />
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="#0BFFFFFF" />
<LinearGradientBrush x:Key="DockItemBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.33" Color="#0FFFFFFF" />
<GradientStop Offset="1.0" Color="#19FFFFFF" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="#0BFFFFFF" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="#80FFFFFF" />
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="#4DFFFFFF" />
<LinearGradientBrush x:Key="DockItemBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.33" Color="#08000000" />
<GradientStop Offset="1.0" Color="#17000000" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="#05000000" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="{StaticResource SystemColorHighlightTextColor}" />
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
<SolidColorBrush x:Key="DockItemBorderBrushPointerOver" Color="{StaticResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Style BasedOn="{StaticResource DefaultDockItemControlStyle}" TargetType="local:DockItemControl" />
<Style x:Key="DefaultDockItemControlStyle" TargetType="local:DockItemControl">
<Style.Setters>
<Setter Property="Background" Value="{ThemeResource DockItemBackground}" />
<Setter Property="BorderBrush" Value="{ThemeResource DockItemBorderBrush}" />
<Setter Property="Padding" Value="4,2,4,2" />
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockItemControl">
<Grid
x:Name="PART_RootGrid"
Padding="{TemplateBinding Padding}"
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
ToolTipService.ToolTip="{TemplateBinding ToolTip}">
<Grid AutomationProperties.Name="{TemplateBinding Title}" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Icon -->
<ContentPresenter
x:Name="IconPresenter"
VerticalAlignment="Center"
Content="{TemplateBinding Icon}" />
<!-- Text (Title + Subtitle) -->
<StackPanel
Grid.Column="1"
Margin="8,0,8,0"
VerticalAlignment="Center">
<TextBlock
x:Name="TitleText"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Text="{TemplateBinding Title}"
TextAlignment="Left"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
x:Name="SubtitleText"
MaxWidth="100"
Margin="0,-4,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="10"
Foreground="{ThemeResource TextFillColorTertiary}"
Text="{TemplateBinding Subtitle}"
TextAlignment="Center"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</StackPanel>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPointerOver}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPressed}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="Transparent" />
<Setter Target="PART_RootGrid.BorderBrush" Value="Transparent" />
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
<Setter Target="TitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
<Setter Target="SubtitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,151 @@
// 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.Input;
using Microsoft.UI.Xaml.Markup;
namespace Microsoft.CmdPal.UI.Dock;
[ContentProperty(Name = nameof(Icon))]
public sealed partial class DockItemControl : Control
{
public DockItemControl()
{
DefaultStyleKey = typeof(DockItemControl);
}
public static readonly DependencyProperty ToolTipProperty =
DependencyProperty.Register(nameof(ToolTip), typeof(string), typeof(DockItemControl), new PropertyMetadata(null));
public string ToolTip
{
get => (string)GetValue(ToolTipProperty);
set => SetValue(ToolTipProperty, value);
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly DependencyProperty SubtitleProperty =
DependencyProperty.Register(nameof(Subtitle), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
public string Subtitle
{
get => (string)GetValue(SubtitleProperty);
set => SetValue(SubtitleProperty, value);
}
public static readonly DependencyProperty IconProperty =
DependencyProperty.Register(nameof(Icon), typeof(object), typeof(DockItemControl), new PropertyMetadata(null, OnIconPropertyChanged));
public object Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
private const string IconPresenterName = "IconPresenter";
private const string TitleTextName = "TitleText";
private const string SubtitleTextName = "SubtitleText";
private FrameworkElement? _iconPresenter;
private FrameworkElement? _titleText;
private FrameworkElement? _subtitleText;
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockItemControl control)
{
control.UpdateTextVisibility();
}
}
private static void OnIconPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockItemControl control)
{
control.UpdateIconVisibility();
}
}
private static bool IsNullOrEmpty(string? value) => string.IsNullOrEmpty(value);
private void UpdateTextVisibility()
{
if (_titleText is not null)
{
_titleText.Visibility = IsNullOrEmpty(Title) ? Visibility.Collapsed : Visibility.Visible;
}
if (_subtitleText is not null)
{
_subtitleText.Visibility = IsNullOrEmpty(Subtitle) ? Visibility.Collapsed : Visibility.Visible;
}
}
private void UpdateIconVisibility()
{
if (_iconPresenter is not null)
{
_iconPresenter.Visibility = Icon is null ? Visibility.Collapsed : Visibility.Visible;
}
}
private void UpdateAllVisibility()
{
UpdateTextVisibility();
UpdateIconVisibility();
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
IsEnabledChanged -= OnIsEnabledChanged;
PointerEntered += Control_PointerEntered;
PointerExited += Control_PointerExited;
IsEnabledChanged += OnIsEnabledChanged;
// Get template children for visibility updates
_iconPresenter = GetTemplateChild(IconPresenterName) as FrameworkElement;
_titleText = GetTemplateChild(TitleTextName) as FrameworkElement;
_subtitleText = GetTemplateChild(SubtitleTextName) as FrameworkElement;
// Set initial visibility
UpdateAllVisibility();
}
private void Control_PointerEntered(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(this, "PointerOver", true);
}
private void Control_PointerExited(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(this, "Normal", true);
}
protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
if (IsEnabled)
{
base.OnPointerPressed(e);
VisualStateManager.GoToState(this, "Pressed", true);
}
}
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
}
}

View File

@@ -30,7 +30,7 @@
</PropertyGroup>
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
<PropertyGroup>
<!-- <PropertyGroup>
<EnableCmdPalAOT>true</EnableCmdPalAOT>
<GeneratePackageLocally>true</GeneratePackageLocally>
</PropertyGroup>
@@ -40,7 +40,7 @@
<PublishSingleFile>false</PublishSingleFile>
<DisableRuntimeMarshalling>false</DisableRuntimeMarshalling>
<PublishAot>true</PublishAot>
</PropertyGroup>
</PropertyGroup> -->
<PropertyGroup Condition="'$(CIBuild)' == 'true' or '$(GeneratePackageLocally)' == 'true'">
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
@@ -79,6 +79,7 @@
<None Remove="Controls\ScreenPreview.xaml" />
<None Remove="Controls\ScrollContainer.xaml" />
<None Remove="Controls\SearchBar.xaml" />
<None Remove="Dock\DockItemControl.xaml" />
<None Remove="IsEnabledTextBlock.xaml" />
<None Remove="ListDetailPage.xaml" />
<None Remove="LoadingPage.xaml" />
@@ -220,6 +221,12 @@
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Dock\DockItemControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ScrollContainer.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -36,6 +36,11 @@
<RepositionThemeTransition IsStaggeringEnabled="False" />
</StackPanel.ChildrenTransitions>-->
<!-- Enable Dock -->
<controls:SettingsCard x:Uid="Settings_GeneralPage_EnableDock_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xF596;}">
<ToggleSwitch IsOn="{x:Bind viewModel.EnableDock, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Appearance Section -->
<TextBlock x:Uid="DockAppearanceSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />

View File

@@ -85,10 +85,6 @@
<ToggleSwitch IsOn="{x:Bind viewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_EnableDock_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xF596;}">
<ToggleSwitch IsOn="{x:Bind viewModel.EnableDock, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- 'For Developers' section -->
<TextBlock x:Uid="ForDevelopersSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />

View File

@@ -390,7 +390,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>Extensions</value>
</data>
<data name="Settings_GeneralPage_NavigationViewItem_Dock.Content" xml:space="preserve">
<value>Dock</value>
<value>Dock (Preview)</value>
</data>
<data name="SettingsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Open Command Palette settings</value>