diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockViewModel.cs index 80a696fa72..2d7d8a2c67 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockViewModel.cs @@ -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(this); @@ -135,6 +137,176 @@ public sealed partial class DockViewModel : IDisposable, return null; } + /// + /// Syncs the band position in settings after a same-list reorder. + /// Does not save to disk - call SaveBandOrder() when done editing. + /// + 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); + } + + /// + /// Moves a dock band to a new position (cross-list drop). + /// Does not save to disk - call SaveBandOrder() when done editing. + /// + 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)"); + } + + /// + /// Saves the current band order to settings. + /// Call this when exiting edit mode. + /// + public void SaveBandOrder() + { + _snapshotStartBands = null; + _snapshotEndBands = null; + SettingsModel.SaveSettings(_settingsModel); + Logger.LogDebug("Saved band order to settings"); + } + + private List? _snapshotStartBands; + private List? _snapshotEndBands; + + /// + /// Takes a snapshot of the current band order before editing. + /// Call this when entering edit mode. + /// + 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"); + } + + /// + /// Restores the band order from the snapshot taken when entering edit mode. + /// Call this when discarding edit mode changes. + /// + 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 ?? ""; @@ -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), }; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs index 3cc1cd2564..5474fdb470 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs @@ -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 } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/EnterDockEditModeMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/EnterDockEditModeMessage.cs new file mode 100644 index 0000000000..b9329ba77e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/EnterDockEditModeMessage.cs @@ -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(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml index c702cb49f4..b826870df8 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml @@ -20,6 +20,7 @@ + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/BandAlignmentConverter.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/BandAlignmentConverter.cs new file mode 100644 index 0000000000..92c1670c24 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/BandAlignmentConverter.cs @@ -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 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(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockBandTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockBandTemplateSelector.cs new file mode 100644 index 0000000000..6d86b7a67d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockBandTemplateSelector.cs @@ -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; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml index fce5ea5fd5..0e6552a456 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml @@ -16,10 +16,12 @@ - + + + + + + - + - + + + - - + - - - - + ItemsPanel="{StaticResource HorizontalItemsPanel}" + ItemsSource="{x:Bind Items, Mode=OneWay}" /> + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml.cs new file mode 100644 index 0000000000..8bc76d57d2 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml.cs @@ -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); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj index 99609e8dc1..c6257d55aa 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -30,7 +30,7 @@ - + true @@ -79,6 +79,7 @@ + @@ -220,6 +221,12 @@ + + + MSBuild:Compile + + + MSBuild:Compile diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/DockSettingsPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/DockSettingsPage.xaml index d0a278e1cf..d797a48ab3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/DockSettingsPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/DockSettingsPage.xaml @@ -36,6 +36,11 @@ --> + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml index 83100377fe..53994b345f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml @@ -85,10 +85,6 @@ - - - - diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw index a63415e28d..0225677f54 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw @@ -390,7 +390,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Extensions - Dock + Dock (Preview) Open Command Palette settings