Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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;

/// <summary>
/// Pure helper methods for sorting and inserting items according to a user-defined
/// extension order. Operates on provider ID strings so the logic is easily testable
/// without constructing heavyweight view-model instances.
/// </summary>
internal static class ExtensionOrderHelper
{
/// <summary>
/// Returns a new list with items sorted so that those whose provider is in
/// <paramref name="extensionOrder"/> appear first (in that order), followed by
/// the remaining items in their original order.
/// </summary>
internal static List<T> SortByExtensionOrder<T>(List<T> items, string[] extensionOrder, Func<T, string> providerIdSelector)
{
var orderLookup = new Dictionary<string, int>(extensionOrder.Length, StringComparer.Ordinal);
for (var i = 0; i < extensionOrder.Length; i++)
{
orderLookup.TryAdd(extensionOrder[i], i);
}

var ordered = new List<T>(items.Count);
var unordered = new List<T>(items.Count);

foreach (var item in items)
{
if (orderLookup.ContainsKey(providerIdSelector(item)))
{
ordered.Add(item);
}
else
{
unordered.Add(item);
}
}

ordered.Sort((a, b) => orderLookup[providerIdSelector(a)].CompareTo(orderLookup[providerIdSelector(b)]));
ordered.AddRange(unordered);
return ordered;
}

/// <summary>
/// Determines where to insert a new provider's items in the existing list based on
/// <paramref name="extensionOrder"/>. If the provider is in the order list, it's
/// placed after other ordered providers that precede it. Otherwise it goes at the end.
/// </summary>
internal static int FindInsertIndex<T>(List<T> items, string providerId, string[] extensionOrder, Func<T, string> providerIdSelector)
{
var providerIndex = Array.IndexOf(extensionOrder, providerId);
if (providerIndex < 0)
{
return items.Count;
}

// Find the last item in the list whose provider has a lower order index
for (var i = items.Count - 1; i >= 0; i--)
{
var existingIndex = Array.IndexOf(extensionOrder, providerIdSelector(items[i]));
if (existingIndex >= 0 && existingIndex < providerIndex)
{
// Insert after the last command of this earlier-ordered provider
var insertAfterProvider = providerIdSelector(items[i]);
for (var j = i + 1; j < items.Count; j++)
{
if (providerIdSelector(items[j]) != insertAfterProvider)
{
return j;
}
}

return i + 1;
}
}

// This provider has the lowest order index among those present — insert at the beginning
return 0;
}
}
10 changes: 10 additions & 0 deletions src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ public string[] FallbackRanks
init => _fallbackRanks = value;
}

private string[]? _extensionOrder = [];

public string[] ExtensionOrder
{
get => _extensionOrder ?? [];
init => _extensionOrder = value;
}

private ImmutableDictionary<string, CommandAlias>? _aliases
= ImmutableDictionary<string, CommandAlias>.Empty;

Expand Down Expand Up @@ -165,12 +173,14 @@ public SettingsModel(
ImmutableList<PinnedCommandSettings>? pinnedCommands = null,
ImmutableDictionary<string, ProviderSettings>? providerSettings = null,
string[]? fallbackRanks = null,
string[]? extensionOrder = null,
ImmutableDictionary<string, CommandAlias>? aliases = null,
ImmutableList<TopLevelHotkey>? commandHotkeys = null)
{
PinnedCommands = pinnedCommands ?? ImmutableList<PinnedCommandSettings>.Empty;
ProviderSettings = providerSettings ?? ImmutableDictionary<string, ProviderSettings>.Empty;
FallbackRanks = fallbackRanks ?? [];
ExtensionOrder = extensionOrder ?? [];
Aliases = aliases ?? ImmutableDictionary<string, CommandAlias>.Empty;
CommandHotkeys = commandHotkeys ?? ImmutableList<TopLevelHotkey>.Empty;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ public bool EnableDock

public ObservableCollection<FallbackSettingsViewModel> FallbackRankings { get; set; } = new();

public ObservableCollection<ProviderSettingsViewModel> ExtensionOrderRankings { get; set; } = new();

public ObservableCollection<DockMonitorConfigViewModel> MonitorConfigs { get; } = new();

public SettingsExtensionsViewModel Extensions { get; }
Expand Down Expand Up @@ -366,6 +368,24 @@ public SettingsViewModel(
}

FallbackRankings = new ObservableCollection<FallbackSettingsViewModel>(fallbackRankings.OrderBy(o => o.Score).Select(fr => fr.Item));

// Build extension order rankings: providers in ExtensionOrder first (in order), then the rest
var currentExtensionOrder = _settingsService.Settings.ExtensionOrder;
var extensionOrderLookup = new Dictionary<string, int>(currentExtensionOrder.Length, StringComparer.Ordinal);
for (var i = 0; i < currentExtensionOrder.Length; i++)
{
extensionOrderLookup.TryAdd(currentExtensionOrder[i], i);
}

var orderedProviders = new List<Scored<ProviderSettingsViewModel>>(CommandProviders.Count);
foreach (var provider in CommandProviders)
{
var score = extensionOrderLookup.TryGetValue(provider.Id, out var idx) ? idx : CommandProviders.Count + orderedProviders.Count;
orderedProviders.Add(new Scored<ProviderSettingsViewModel>() { Item = provider, Score = score });
}

ExtensionOrderRankings = new ObservableCollection<ProviderSettingsViewModel>(orderedProviders.OrderBy(o => o.Score).Select(o => o.Item));

Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler);

if (needsSave)
Expand Down Expand Up @@ -393,6 +413,13 @@ public void ApplyFallbackSort()
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FallbackRankings)));
}

public void ApplyExtensionOrder()
{
_settingsService.UpdateSettings(s => s with { ExtensionOrder = ExtensionOrderRankings.Select(p => p.Id).ToArray() });
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExtensionOrderRankings)));
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>();
}

/// <summary>
/// Builds or refreshes the <see cref="MonitorConfigs"/> collection by reconciling
/// connected monitors with persisted per-monitor settings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ private async Task UpdateCommandsForProvider(CommandProviderWrapper sender, IIte

var startIndex = FindIndexForFirstProviderItem(clone, sender.ProviderId);
clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);

if (startIndex >= clone.Count)
{
// Provider wasn't in the list yet — find position based on ExtensionOrder
var extensionOrder = _serviceProvider.GetRequiredService<ISettingsService>().Settings.ExtensionOrder;
startIndex = FindInsertIndexByExtensionOrder(clone, sender.ProviderId, extensionOrder);
}

clone.InsertRange(startIndex, newItems);

ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
Expand Down Expand Up @@ -469,6 +477,12 @@ private async Task<RegisterAndLoadSummary> RegisterAndLoadCommandsAsync(IEnumera

lock (TopLevelCommands)
{
var extensionOrder = _serviceProvider.GetRequiredService<ISettingsService>().Settings.ExtensionOrder;
if (extensionOrder.Length > 0)
{
commandsToAdd = SortByExtensionOrder(commandsToAdd, extensionOrder);
}

foreach (var c in commandsToAdd)
{
TopLevelCommands.Add(c);
Expand All @@ -495,6 +509,12 @@ private async Task<RegisterAndLoadSummary> RegisterAndLoadCommandsAsync(IEnumera
return new RegisterAndLoadSummary(totalCommands, totalDockBands);
}

internal static List<TopLevelViewModel> SortByExtensionOrder(List<TopLevelViewModel> commands, string[] extensionOrder)
=> ExtensionOrderHelper.SortByExtensionOrder(commands, extensionOrder, c => c.CommandProviderId);

private static int FindInsertIndexByExtensionOrder(List<TopLevelViewModel> items, string providerId, string[] extensionOrder)
=> ExtensionOrderHelper.FindInsertIndex(items, providerId, extensionOrder, c => c.CommandProviderId);

private async Task<CommandLoadResult> TryLoadCommandsAsync(CommandProviderWrapper wrapper, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ExtensionRanker"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Microsoft.CmdPal.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
mc:Ignorable="d">

<Grid>
<ListView
Padding="12,0,24,0"
AllowDrop="True"
CanDragItems="True"
CanReorderItems="True"
DragItemsCompleted="ListView_DragItemsCompleted"
ItemsSource="{x:Bind viewModel.ExtensionOrderRankings, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="viewModels:ProviderSettingsViewModel">
<Grid Padding="4,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Viewbox Grid.Column="1" Height="18">
<PathIcon Data="M15.5 17C16.3284 17 17 17.6716 17 18.5C17 19.3284 16.3284 20 15.5 20C14.6716 20 14 19.3284 14 18.5C14 17.6716 14.6716 17 15.5 17ZM8.5 17C9.32843 17 10 17.6716 10 18.5C10 19.3284 9.32843 20 8.5 20C7.67157 20 7 19.3284 7 18.5C7 17.6716 7.67157 17 8.5 17ZM15.5 10C16.3284 10 17 10.6716 17 11.5C17 12.3284 16.3284 13 15.5 13C14.6716 13 14 12.3284 14 11.5C14 10.6716 14.6716 10 15.5 10ZM8.5 10C9.32843 10 10 10.6716 10 11.5C10 12.3284 9.32843 13 8.5 13C7.67157 13 7 12.3284 7 11.5C7 10.6716 7.67157 10 8.5 10ZM15.5 3C16.3284 3 17 3.67157 17 4.5C17 5.32843 16.3284 6 15.5 6C14.6716 6 14 5.32843 14 4.5C14 3.67157 14.6716 3 15.5 3ZM8.5 3C9.32843 3 10 3.67157 10 4.5C10 5.32843 9.32843 6 8.5 6C7.67157 6 7 5.32843 7 4.5C7 3.67157 7.67157 3 8.5 3Z" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</Viewbox>
<controls:SettingsCard
Width="560"
MinHeight="0"
Padding="8"
Background="Transparent"
BorderThickness="0"
Header="{x:Bind DisplayName}"
ToolTipService.ToolTip="{x:Bind Id}">
<controls:SettingsCard.HeaderIcon>
<cpcontrols:ContentIcon>
<cpcontrols:ContentIcon.Content>
<cpcontrols:IconBox
Width="16"
Height="16"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
</cpcontrols:ContentIcon.Content>
</cpcontrols:ContentIcon>
</controls:SettingsCard.HeaderIcon>
</controls:SettingsCard>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" Spacing="0" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="4" />
</Style>
</ListView.ItemContainerStyle>
</ListView>
</Grid>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;

namespace Microsoft.CmdPal.UI.Controls;

public sealed partial class ExtensionRanker : UserControl
{
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
private SettingsViewModel? viewModel;

public ExtensionRanker()
{
this.InitializeComponent();

var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var themeService = App.Current.Services.GetService<IThemeService>()!;
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
}

private void ListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
{
viewModel?.ApplyExtensionOrder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ExtensionRankerDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ContentDialog
x:Name="ExtensionRankerContentDialog"
Width="420"
MinWidth="420"
PrimaryButtonText="OK">
<ContentDialog.Title>
<TextBlock x:Uid="ManageExtensionOrder" />
</ContentDialog.Title>
<ContentDialog.Resources>
<x:Double x:Key="ContentDialogMaxWidth">800</x:Double>
</ContentDialog.Resources>
<Grid Width="560" MinWidth="420">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="ManageExtensionOrderDialogDescription"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
<Rectangle
Grid.Row="1"
Height="1"
Margin="0,16,0,16"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<cpcontrols:ExtensionRanker
x:Name="ExtensionRanker"
Grid.Row="2"
Margin="-24,0,-24,0" />
</Grid>
</ContentDialog>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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.Controls;
using Windows.Foundation;

namespace Microsoft.CmdPal.UI.Controls;

public sealed partial class ExtensionRankerDialog : UserControl
{
public ExtensionRankerDialog()
{
InitializeComponent();
}

public IAsyncOperation<ContentDialogResult> ShowAsync()
{
return ExtensionRankerContentDialog!.ShowAsync()!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@
<FontIcon Glyph="&#xE8CB;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem x:Uid="Settings_ExtensionsPage_More_ReorderExtensions_MenuFlyoutItem" Click="ReorderExtensions_OnClick">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE174;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
</MenuFlyout>
</Button.Flyout>
<FontIcon
Expand Down Expand Up @@ -264,6 +269,7 @@
</Grid>
</ScrollViewer>
<cpcontrols:FallbackRankerDialog x:Name="FallbackRankerDialog" />
<cpcontrols:ExtensionRankerDialog x:Name="ExtensionRankerDialog" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="LayoutVisualStates">
<VisualState x:Name="WideLayout">
Expand Down
Loading
Loading