mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-06 03:07:04 +02:00
CmdPal: Make settings and app state immutable (#46451)
## Summary This PR refactors CmdPal settings/state to be immutable end-to-end. ### Core changes - Convert model types to immutable records / init-only properties: - `SettingsModel` - `AppStateModel` - `ProviderSettings` - `DockSettings` - `RecentCommandsManager` - supporting settings types (fallback/hotkey/alias/top-level hotkey/history items, etc.) - Replace mutable collections with immutable equivalents where appropriate: - `ImmutableDictionary<,>` - `ImmutableList<>` - Move mutation flow to atomic service updates: - `ISettingsService.UpdateSettings(Func<SettingsModel, SettingsModel>)` - `IAppStateService.UpdateState(Func<AppStateModel, AppStateModel>)` - Update ViewModels/managers/services to use copy-on-write (`with`) patterns instead of in-place mutation. - Update serialization context + tests for immutable model graph compatibility. ## Why Issue #46437 is caused by mutable shared state being updated from different execution paths/threads, leading to race-prone behavior during persistence/serialization. By making settings/app state immutable and using atomic swap/update patterns, we remove in-place mutation and eliminate this class of concurrency bug. ## Validation - Built successfully: - `Microsoft.CmdPal.UI.ViewModels` - `Microsoft.CmdPal.UI` - `Microsoft.CmdPal.UI.ViewModels.UnitTests` - Updated unit tests for immutable update patterns. Fixes #46437 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Immutable;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
@@ -14,26 +15,32 @@ public partial class AliasManager : ObservableObject
|
|||||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
|
|
||||||
// REMEMBER, CommandAlias.SearchPrefix is what we use as keys
|
private static readonly ImmutableList<CommandAlias> _defaultAliases = new List<CommandAlias>
|
||||||
private readonly Dictionary<string, CommandAlias> _aliases;
|
{
|
||||||
|
new CommandAlias(":", "com.microsoft.cmdpal.registry", true),
|
||||||
|
new CommandAlias("$", "com.microsoft.cmdpal.windowsSettings", true),
|
||||||
|
new CommandAlias("=", "com.microsoft.cmdpal.calculator", true),
|
||||||
|
new CommandAlias(">", "com.microsoft.cmdpal.shell", true),
|
||||||
|
new CommandAlias("<", "com.microsoft.cmdpal.windowwalker", true),
|
||||||
|
new CommandAlias("??", "com.microsoft.cmdpal.websearch", true),
|
||||||
|
new CommandAlias("file", "com.microsoft.indexer.fileSearch", false),
|
||||||
|
new CommandAlias(")", "com.microsoft.cmdpal.timedate", true),
|
||||||
|
}.ToImmutableList();
|
||||||
|
|
||||||
public AliasManager(TopLevelCommandManager tlcManager, ISettingsService settingsService)
|
public AliasManager(TopLevelCommandManager tlcManager, ISettingsService settingsService)
|
||||||
{
|
{
|
||||||
_topLevelCommandManager = tlcManager;
|
_topLevelCommandManager = tlcManager;
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_aliases = _settingsService.Settings.Aliases;
|
|
||||||
|
|
||||||
if (_aliases.Count == 0)
|
if (_settingsService.Settings.Aliases.Count == 0)
|
||||||
{
|
{
|
||||||
PopulateDefaultAliases();
|
PopulateDefaultAliases();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddAlias(CommandAlias a) => _aliases.Add(a.SearchPrefix, a);
|
|
||||||
|
|
||||||
public bool CheckAlias(string searchText)
|
public bool CheckAlias(string searchText)
|
||||||
{
|
{
|
||||||
if (_aliases.TryGetValue(searchText, out var alias))
|
if (_settingsService.Settings.Aliases.TryGetValue(searchText, out var alias))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -56,19 +63,18 @@ public partial class AliasManager : ObservableObject
|
|||||||
|
|
||||||
private void PopulateDefaultAliases()
|
private void PopulateDefaultAliases()
|
||||||
{
|
{
|
||||||
this.AddAlias(new CommandAlias(":", "com.microsoft.cmdpal.registry", true));
|
_settingsService.UpdateSettings(
|
||||||
this.AddAlias(new CommandAlias("$", "com.microsoft.cmdpal.windowsSettings", true));
|
s => s with
|
||||||
this.AddAlias(new CommandAlias("=", "com.microsoft.cmdpal.calculator", true));
|
{
|
||||||
this.AddAlias(new CommandAlias(">", "com.microsoft.cmdpal.shell", true));
|
Aliases = s.Aliases
|
||||||
this.AddAlias(new CommandAlias("<", "com.microsoft.cmdpal.windowwalker", true));
|
.AddRange(_defaultAliases.ToDictionary(a => a.SearchPrefix, a => a)),
|
||||||
this.AddAlias(new CommandAlias("??", "com.microsoft.cmdpal.websearch", true));
|
},
|
||||||
this.AddAlias(new CommandAlias("file", "com.microsoft.indexer.fileSearch", false));
|
hotReload: false);
|
||||||
this.AddAlias(new CommandAlias(")", "com.microsoft.cmdpal.timedate", true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? KeysFromId(string commandId)
|
public string? KeysFromId(string commandId)
|
||||||
{
|
{
|
||||||
return _aliases
|
return _settingsService.Settings.Aliases
|
||||||
.Where(kv => kv.Value.CommandId == commandId)
|
.Where(kv => kv.Value.CommandId == commandId)
|
||||||
.Select(kv => kv.Value.Alias)
|
.Select(kv => kv.Value.Alias)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
@@ -76,7 +82,7 @@ public partial class AliasManager : ObservableObject
|
|||||||
|
|
||||||
public CommandAlias? AliasFromId(string commandId)
|
public CommandAlias? AliasFromId(string commandId)
|
||||||
{
|
{
|
||||||
return _aliases
|
return _settingsService.Settings.Aliases
|
||||||
.Where(kv => kv.Value.CommandId == commandId)
|
.Where(kv => kv.Value.CommandId == commandId)
|
||||||
.Select(kv => kv.Value)
|
.Select(kv => kv.Value)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
@@ -90,9 +96,11 @@ public partial class AliasManager : ObservableObject
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var aliases = _settingsService.Settings.Aliases;
|
||||||
|
|
||||||
// If we already have _this exact alias_, do nothing
|
// If we already have _this exact alias_, do nothing
|
||||||
if (newAlias is not null &&
|
if (newAlias is not null &&
|
||||||
_aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
|
aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
|
||||||
{
|
{
|
||||||
if (existingAlias.CommandId == commandId)
|
if (existingAlias.CommandId == commandId)
|
||||||
{
|
{
|
||||||
@@ -100,19 +108,19 @@ public partial class AliasManager : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<CommandAlias> toRemove = [];
|
var keysToRemove = new List<string>();
|
||||||
foreach (var kv in _aliases)
|
foreach (var kv in aliases)
|
||||||
{
|
{
|
||||||
// Look for the old aliases for the command, and remove it
|
// Look for the old aliases for the command, and remove it
|
||||||
if (kv.Value.CommandId == commandId)
|
if (kv.Value.CommandId == commandId)
|
||||||
{
|
{
|
||||||
toRemove.Add(kv.Value);
|
keysToRemove.Add(kv.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for the alias belonging to another command, and remove it
|
// Look for the alias belonging to another command, and remove it
|
||||||
if (newAlias is not null && kv.Value.Alias == newAlias.Alias && kv.Value.CommandId != commandId)
|
if (newAlias is not null && kv.Value.Alias == newAlias.Alias && kv.Value.CommandId != commandId)
|
||||||
{
|
{
|
||||||
toRemove.Add(kv.Value);
|
keysToRemove.Add(kv.Key);
|
||||||
|
|
||||||
// Remove alias from other TopLevelViewModels it may be assigned to
|
// Remove alias from other TopLevelViewModels it may be assigned to
|
||||||
var topLevelCommand = _topLevelCommandManager.LookupCommand(kv.Value.CommandId);
|
var topLevelCommand = _topLevelCommandManager.LookupCommand(kv.Value.CommandId);
|
||||||
@@ -123,15 +131,16 @@ public partial class AliasManager : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var alias in toRemove)
|
_settingsService.UpdateSettings(s =>
|
||||||
{
|
{
|
||||||
// REMEMBER, SearchPrefix is what we use as keys
|
var updatedAliases = s.Aliases.RemoveRange(keysToRemove);
|
||||||
_aliases.Remove(alias.SearchPrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newAlias is not null)
|
if (newAlias is not null)
|
||||||
{
|
{
|
||||||
AddAlias(newAlias);
|
updatedAliases = updatedAliases.Add(newAlias.SearchPrefix, newAlias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return s with { Aliases = updatedAliases };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,18 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Collections.Immutable;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
public partial class AppStateModel : ObservableObject
|
public record AppStateModel
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// STATE HERE
|
// STATE HERE
|
||||||
// Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)!
|
|
||||||
// Make sure that any new types you add are added to JsonSerializationContext!
|
// Make sure that any new types you add are added to JsonSerializationContext!
|
||||||
public RecentCommandsManager RecentCommands { get; set; } = new();
|
public RecentCommandsManager RecentCommands { get; init; } = new();
|
||||||
|
|
||||||
public List<string> RunHistory { get; set; } = [];
|
public ImmutableList<string> RunHistory { get; init; } = ImmutableList<string>.Empty;
|
||||||
|
|
||||||
// END SETTINGS
|
// END SETTINGS
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -112,10 +112,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.Theme != value)
|
if (_settingsService.Settings.Theme != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.Theme = value;
|
_settingsService.UpdateSettings(s => s with { Theme = value });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(ThemeIndex));
|
OnPropertyChanged(nameof(ThemeIndex));
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.ColorizationMode != value)
|
if (_settingsService.Settings.ColorizationMode != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.ColorizationMode = value;
|
_settingsService.UpdateSettings(s => s with { ColorizationMode = value });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||||
@@ -146,7 +146,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
|
|
||||||
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
||||||
|
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.CustomThemeColor != value)
|
if (_settingsService.Settings.CustomThemeColor != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.CustomThemeColor = value;
|
_settingsService.UpdateSettings(s => s with { CustomThemeColor = value });
|
||||||
|
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
ColorIntensity = 100;
|
ColorIntensity = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,10 +183,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
get => _settingsService.Settings.CustomThemeColorIntensity;
|
get => _settingsService.Settings.CustomThemeColorIntensity;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.CustomThemeColorIntensity = value;
|
_settingsService.UpdateSettings(s => s with { CustomThemeColorIntensity = value });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,10 +195,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
get => _settingsService.Settings.BackgroundImageTintIntensity;
|
get => _settingsService.Settings.BackgroundImageTintIntensity;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.BackgroundImageTintIntensity = value;
|
_settingsService.UpdateSettings(s => s with { BackgroundImageTintIntensity = value });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.BackgroundImagePath != value)
|
if (_settingsService.Settings.BackgroundImagePath != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.BackgroundImagePath = value;
|
_settingsService.UpdateSettings(s => s with { BackgroundImagePath = value });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
|
||||||
if (BackgroundImageOpacity == 0)
|
if (BackgroundImageOpacity == 0)
|
||||||
@@ -217,7 +217,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
BackgroundImageOpacity = 100;
|
BackgroundImageOpacity = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,9 +229,9 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.BackgroundImageOpacity != value)
|
if (_settingsService.Settings.BackgroundImageOpacity != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.BackgroundImageOpacity = value;
|
_settingsService.UpdateSettings(s => s with { BackgroundImageOpacity = value });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,9 +243,9 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.BackgroundImageBrightness != value)
|
if (_settingsService.Settings.BackgroundImageBrightness != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.BackgroundImageBrightness = value;
|
_settingsService.UpdateSettings(s => s with { BackgroundImageBrightness = value });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,9 +257,9 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.BackgroundImageBlurAmount != value)
|
if (_settingsService.Settings.BackgroundImageBlurAmount != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.BackgroundImageBlurAmount = value;
|
_settingsService.UpdateSettings(s => s with { BackgroundImageBlurAmount = value });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,10 +271,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.BackgroundImageFit != value)
|
if (_settingsService.Settings.BackgroundImageFit != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.BackgroundImageFit = value;
|
_settingsService.UpdateSettings(s => s with { BackgroundImageFit = value });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,11 +305,11 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.BackdropOpacity != value)
|
if (_settingsService.Settings.BackdropOpacity != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.BackdropOpacity = value;
|
_settingsService.UpdateSettings(s => s with { BackdropOpacity = value });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(EffectiveBackdropStyle));
|
OnPropertyChanged(nameof(EffectiveBackdropStyle));
|
||||||
OnPropertyChanged(nameof(EffectiveImageOpacity));
|
OnPropertyChanged(nameof(EffectiveImageOpacity));
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +322,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
var newStyle = (BackdropStyle)value;
|
var newStyle = (BackdropStyle)value;
|
||||||
if (_settingsService.Settings.BackdropStyle != newStyle)
|
if (_settingsService.Settings.BackdropStyle != newStyle)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.BackdropStyle = newStyle;
|
_settingsService.UpdateSettings(s => s with { BackdropStyle = newStyle });
|
||||||
|
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(IsBackdropOpacityVisible));
|
OnPropertyChanged(nameof(IsBackdropOpacityVisible));
|
||||||
@@ -335,7 +335,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
IsColorizationDetailsExpanded = false;
|
IsColorizationDetailsExpanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,9 +468,8 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Save()
|
private void DebouncedReapply()
|
||||||
{
|
{
|
||||||
_settingsService.Save();
|
|
||||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
public class CommandAlias
|
public record CommandAlias
|
||||||
{
|
{
|
||||||
public string CommandId { get; set; }
|
public string CommandId { get; init; }
|
||||||
|
|
||||||
public string Alias { get; set; }
|
public string Alias { get; init; }
|
||||||
|
|
||||||
public bool IsDirect { get; set; }
|
public bool IsDirect { get; init; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string SearchPrefix => Alias + (IsDirect ? string.Empty : " ");
|
public string SearchPrefix => Alias + (IsDirect ? string.Empty : " ");
|
||||||
|
|||||||
@@ -127,7 +127,12 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
|||||||
|
|
||||||
private ProviderSettings GetProviderSettings(SettingsModel settings)
|
private ProviderSettings GetProviderSettings(SettingsModel settings)
|
||||||
{
|
{
|
||||||
return settings.GetProviderSettings(this);
|
if (!settings.ProviderSettings.TryGetValue(ProviderId, out var ps))
|
||||||
|
{
|
||||||
|
ps = new ProviderSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ps.WithConnection(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadTopLevelCommands(IServiceProvider serviceProvider)
|
public async Task LoadTopLevelCommands(IServiceProvider serviceProvider)
|
||||||
@@ -140,9 +145,26 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||||
var settings = settingsService.Settings;
|
var providerSettings = GetProviderSettings(settingsService.Settings);
|
||||||
|
|
||||||
|
// Persist the connected provider settings (fallback commands, etc.)
|
||||||
|
settingsService.UpdateSettings(
|
||||||
|
s =>
|
||||||
|
{
|
||||||
|
if (!s.ProviderSettings.TryGetValue(ProviderId, out var ps))
|
||||||
|
{
|
||||||
|
ps = new ProviderSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPs = ps.WithConnection(this);
|
||||||
|
|
||||||
|
return s with
|
||||||
|
{
|
||||||
|
ProviderSettings = s.ProviderSettings.SetItem(ProviderId, newPs),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hotReload: false);
|
||||||
|
|
||||||
var providerSettings = GetProviderSettings(settings);
|
|
||||||
IsActive = providerSettings.IsEnabled;
|
IsActive = providerSettings.IsEnabled;
|
||||||
if (!IsActive)
|
if (!IsActive)
|
||||||
{
|
{
|
||||||
@@ -419,32 +441,59 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
|||||||
public void PinCommand(string commandId, IServiceProvider serviceProvider)
|
public void PinCommand(string commandId, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||||
var settings = settingsService.Settings;
|
var providerSettings = GetProviderSettings(settingsService.Settings);
|
||||||
var providerSettings = GetProviderSettings(settings);
|
|
||||||
|
|
||||||
if (!providerSettings.PinnedCommandIds.Contains(commandId))
|
if (!providerSettings.PinnedCommandIds.Contains(commandId))
|
||||||
{
|
{
|
||||||
providerSettings.PinnedCommandIds.Add(commandId);
|
settingsService.UpdateSettings(
|
||||||
|
s =>
|
||||||
|
{
|
||||||
|
if (!s.ProviderSettings.TryGetValue(ProviderId, out var ps))
|
||||||
|
{
|
||||||
|
ps = new ProviderSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerSettings = ps.WithConnection(this);
|
||||||
|
var newPinned = providerSettings.PinnedCommandIds.Add(commandId);
|
||||||
|
var newPs = providerSettings with { PinnedCommandIds = newPinned };
|
||||||
|
|
||||||
|
return s with
|
||||||
|
{
|
||||||
|
ProviderSettings = s.ProviderSettings.SetItem(ProviderId, newPs),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hotReload: false);
|
||||||
|
|
||||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||||
settingsService.Save(hotReload: false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UnpinCommand(string commandId, IServiceProvider serviceProvider)
|
public void UnpinCommand(string commandId, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||||
var settings = settingsService.Settings;
|
|
||||||
var providerSettings = GetProviderSettings(settings);
|
|
||||||
|
|
||||||
if (providerSettings.PinnedCommandIds.Remove(commandId))
|
settingsService.UpdateSettings(
|
||||||
{
|
s =>
|
||||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
{
|
||||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
if (!s.ProviderSettings.TryGetValue(ProviderId, out var ps))
|
||||||
|
{
|
||||||
|
ps = new ProviderSettings();
|
||||||
|
}
|
||||||
|
|
||||||
settingsService.Save(hotReload: false);
|
var providerSettings = ps.WithConnection(this);
|
||||||
}
|
var newPinned = providerSettings.PinnedCommandIds.Remove(commandId);
|
||||||
|
var newPs = providerSettings with { PinnedCommandIds = newPinned };
|
||||||
|
|
||||||
|
return s with
|
||||||
|
{
|
||||||
|
ProviderSettings = s.ProviderSettings.SetItem(ProviderId, newPs),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hotReload: false);
|
||||||
|
|
||||||
|
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||||
|
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PinDockBand(string commandId, IServiceProvider serviceProvider, Dock.DockPinSide side = Dock.DockPinSide.Start, bool? showTitles = null, bool? showSubtitles = null)
|
public void PinDockBand(string commandId, IServiceProvider serviceProvider, Dock.DockPinSide side = Dock.DockPinSide.Start, bool? showTitles = null, bool? showSubtitles = null)
|
||||||
@@ -470,37 +519,47 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
|||||||
ShowSubtitles = showSubtitles,
|
ShowSubtitles = showSubtitles,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (side)
|
settingsService.UpdateSettings(
|
||||||
{
|
s =>
|
||||||
case Dock.DockPinSide.Center:
|
{
|
||||||
settings.DockSettings.CenterBands.Add(bandSettings);
|
var dockSettings = s.DockSettings;
|
||||||
break;
|
return s with
|
||||||
case Dock.DockPinSide.End:
|
{
|
||||||
settings.DockSettings.EndBands.Add(bandSettings);
|
DockSettings = side switch
|
||||||
break;
|
{
|
||||||
case Dock.DockPinSide.Start:
|
Dock.DockPinSide.Center => dockSettings with { CenterBands = dockSettings.CenterBands.Add(bandSettings) },
|
||||||
default:
|
Dock.DockPinSide.End => dockSettings with { EndBands = dockSettings.EndBands.Add(bandSettings) },
|
||||||
settings.DockSettings.StartBands.Add(bandSettings);
|
_ => dockSettings with { StartBands = dockSettings.StartBands.Add(bandSettings) },
|
||||||
break;
|
},
|
||||||
}
|
};
|
||||||
|
},
|
||||||
|
hotReload: false);
|
||||||
|
|
||||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||||
|
|
||||||
settingsService.Save(hotReload: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UnpinDockBand(string commandId, IServiceProvider serviceProvider)
|
public void UnpinDockBand(string commandId, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||||
var settings = settingsService.Settings;
|
settingsService.UpdateSettings(
|
||||||
settings.DockSettings.StartBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
s =>
|
||||||
settings.DockSettings.CenterBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
{
|
||||||
settings.DockSettings.EndBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
var dockSettings = s.DockSettings;
|
||||||
|
return s with
|
||||||
|
{
|
||||||
|
DockSettings = dockSettings with
|
||||||
|
{
|
||||||
|
StartBands = dockSettings.StartBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId),
|
||||||
|
CenterBands = dockSettings.CenterBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId),
|
||||||
|
EndBands = dockSettings.EndBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hotReload: false);
|
||||||
|
|
||||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||||
settingsService.Save(hotReload: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICommandProviderContext GetProviderContext() => this;
|
public ICommandProviderContext GetProviderContext() => this;
|
||||||
|
|||||||
@@ -679,9 +679,10 @@ public sealed partial class MainListPage : DynamicListPage,
|
|||||||
public void UpdateHistory(IListItem topLevelOrAppItem)
|
public void UpdateHistory(IListItem topLevelOrAppItem)
|
||||||
{
|
{
|
||||||
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
||||||
var history = _appStateService.State.RecentCommands;
|
_appStateService.UpdateState(state => state with
|
||||||
history.AddHistoryItem(id);
|
{
|
||||||
_appStateService.Save();
|
RecentCommands = state.RecentCommands.WithHistoryItem(id),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
@@ -14,10 +15,11 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
private static readonly CompositeFormat PluralItemsFormatString = CompositeFormat.Parse(Properties.Resources.dock_item_count_plural);
|
private static readonly CompositeFormat PluralItemsFormatString = CompositeFormat.Parse(Properties.Resources.dock_item_count_plural);
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly DockBandSettings _dockSettingsModel;
|
|
||||||
private readonly TopLevelViewModel _adapter;
|
private readonly TopLevelViewModel _adapter;
|
||||||
private readonly DockBandViewModel? _bandViewModel;
|
private readonly DockBandViewModel? _bandViewModel;
|
||||||
|
|
||||||
|
private DockBandSettings _dockSettingsModel;
|
||||||
|
|
||||||
public string Title => _adapter.Title;
|
public string Title => _adapter.Title;
|
||||||
|
|
||||||
public string Description
|
public string Description
|
||||||
@@ -54,14 +56,14 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
|||||||
if (value != _showLabels)
|
if (value != _showLabels)
|
||||||
{
|
{
|
||||||
_showLabels = value;
|
_showLabels = value;
|
||||||
_dockSettingsModel.ShowLabels = value switch
|
var newShowTitles = value switch
|
||||||
{
|
{
|
||||||
ShowLabelsOption.Default => null,
|
ShowLabelsOption.Default => (bool?)null,
|
||||||
ShowLabelsOption.ShowLabels => true,
|
ShowLabelsOption.ShowLabels => true,
|
||||||
ShowLabelsOption.HideLabels => false,
|
ShowLabelsOption.HideLabels => false,
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
Save();
|
UpdateModel(_dockSettingsModel with { ShowTitles = newShowTitles });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,9 +176,38 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
|||||||
return bandVm.Items.Count;
|
return bandVm.Items.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Save()
|
private void UpdateModel(DockBandSettings newModel)
|
||||||
{
|
{
|
||||||
_settingsService.Save();
|
var commandId = _dockSettingsModel.CommandId;
|
||||||
|
_settingsService.UpdateSettings(
|
||||||
|
s =>
|
||||||
|
{
|
||||||
|
var dockSettings = s.DockSettings;
|
||||||
|
return s with
|
||||||
|
{
|
||||||
|
DockSettings = dockSettings with
|
||||||
|
{
|
||||||
|
StartBands = ReplaceInList(dockSettings.StartBands, commandId, newModel),
|
||||||
|
CenterBands = ReplaceInList(dockSettings.CenterBands, commandId, newModel),
|
||||||
|
EndBands = ReplaceInList(dockSettings.EndBands, commandId, newModel),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hotReload: false);
|
||||||
|
_dockSettingsModel = newModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableList<DockBandSettings> ReplaceInList(ImmutableList<DockBandSettings> list, string commandId, DockBandSettings newModel)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
if (list[i].CommandId == commandId)
|
||||||
|
{
|
||||||
|
return list.SetItem(i, newModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdatePinSide(DockPinSide value)
|
private void UpdatePinSide(DockPinSide value)
|
||||||
@@ -189,44 +220,31 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
|||||||
|
|
||||||
public void SetBandPosition(DockPinSide side, int? index)
|
public void SetBandPosition(DockPinSide side, int? index)
|
||||||
{
|
{
|
||||||
var dockSettings = _settingsService.Settings.DockSettings;
|
var commandId = _dockSettingsModel.CommandId;
|
||||||
|
|
||||||
// Remove from all sides first
|
_settingsService.UpdateSettings(s =>
|
||||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == _dockSettingsModel.CommandId);
|
|
||||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == _dockSettingsModel.CommandId);
|
|
||||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == _dockSettingsModel.CommandId);
|
|
||||||
|
|
||||||
// Add to the selected side
|
|
||||||
switch (side)
|
|
||||||
{
|
{
|
||||||
case DockPinSide.Start:
|
var dockSettings = s.DockSettings;
|
||||||
{
|
|
||||||
var insertIndex = index ?? dockSettings.StartBands.Count;
|
|
||||||
dockSettings.StartBands.Insert(insertIndex, _dockSettingsModel);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case DockPinSide.Center:
|
// Remove from all sides first
|
||||||
{
|
var newDock = dockSettings with
|
||||||
var insertIndex = index ?? dockSettings.CenterBands.Count;
|
{
|
||||||
dockSettings.CenterBands.Insert(insertIndex, _dockSettingsModel);
|
StartBands = dockSettings.StartBands.RemoveAll(b => b.CommandId == commandId),
|
||||||
break;
|
CenterBands = dockSettings.CenterBands.RemoveAll(b => b.CommandId == commandId),
|
||||||
}
|
EndBands = dockSettings.EndBands.RemoveAll(b => b.CommandId == commandId),
|
||||||
|
};
|
||||||
|
|
||||||
case DockPinSide.End:
|
// Add to the selected side
|
||||||
{
|
newDock = side switch
|
||||||
var insertIndex = index ?? dockSettings.EndBands.Count;
|
{
|
||||||
dockSettings.EndBands.Insert(insertIndex, _dockSettingsModel);
|
DockPinSide.Start => newDock with { StartBands = newDock.StartBands.Insert(index ?? newDock.StartBands.Count, _dockSettingsModel) },
|
||||||
break;
|
DockPinSide.Center => newDock with { CenterBands = newDock.CenterBands.Insert(index ?? newDock.CenterBands.Count, _dockSettingsModel) },
|
||||||
}
|
DockPinSide.End => newDock with { EndBands = newDock.EndBands.Insert(index ?? newDock.EndBands.Count, _dockSettingsModel) },
|
||||||
|
_ => newDock,
|
||||||
|
};
|
||||||
|
|
||||||
case DockPinSide.None:
|
return s with { DockSettings = newDock };
|
||||||
default:
|
});
|
||||||
// Do nothing
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPinSideChanged(DockPinSide value)
|
private void OnPinSideChanged(DockPinSide value)
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
@@ -15,11 +17,11 @@ namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
|||||||
public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
||||||
{
|
{
|
||||||
private readonly CommandItemViewModel _rootItem;
|
private readonly CommandItemViewModel _rootItem;
|
||||||
private readonly DockBandSettings _bandSettings;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly DockSettings _dockSettings;
|
|
||||||
private readonly Action _saveSettings;
|
|
||||||
private readonly IContextMenuFactory _contextMenuFactory;
|
private readonly IContextMenuFactory _contextMenuFactory;
|
||||||
|
|
||||||
|
private DockBandSettings _bandSettings;
|
||||||
|
|
||||||
public ObservableCollection<DockItemViewModel> Items { get; } = new();
|
public ObservableCollection<DockItemViewModel> Items { get; } = new();
|
||||||
|
|
||||||
private bool _showTitles = true;
|
private bool _showTitles = true;
|
||||||
@@ -103,8 +105,7 @@ public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal void SaveShowLabels()
|
internal void SaveShowLabels()
|
||||||
{
|
{
|
||||||
_bandSettings.ShowTitles = _showTitles;
|
ReplaceBandInSettings(_bandSettings with { ShowTitles = _showTitles, ShowSubtitles = _showSubtitles });
|
||||||
_bandSettings.ShowSubtitles = _showSubtitles;
|
|
||||||
_showTitlesSnapshot = null;
|
_showTitlesSnapshot = null;
|
||||||
_showSubtitlesSnapshot = null;
|
_showSubtitlesSnapshot = null;
|
||||||
}
|
}
|
||||||
@@ -127,21 +128,54 @@ public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReplaceBandInSettings(DockBandSettings newSettings)
|
||||||
|
{
|
||||||
|
var commandId = _bandSettings.CommandId;
|
||||||
|
_settingsService.UpdateSettings(
|
||||||
|
s =>
|
||||||
|
{
|
||||||
|
var dockSettings = s.DockSettings;
|
||||||
|
return s with
|
||||||
|
{
|
||||||
|
DockSettings = dockSettings with
|
||||||
|
{
|
||||||
|
StartBands = ReplaceBandInList(dockSettings.StartBands, commandId, newSettings),
|
||||||
|
CenterBands = ReplaceBandInList(dockSettings.CenterBands, commandId, newSettings),
|
||||||
|
EndBands = ReplaceBandInList(dockSettings.EndBands, commandId, newSettings),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false);
|
||||||
|
_bandSettings = newSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableList<DockBandSettings> ReplaceBandInList(ImmutableList<DockBandSettings> list, string commandId, DockBandSettings newSettings)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
if (list[i].CommandId == commandId)
|
||||||
|
{
|
||||||
|
return list.SetItem(i, newSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
internal DockBandViewModel(
|
internal DockBandViewModel(
|
||||||
CommandItemViewModel commandItemViewModel,
|
CommandItemViewModel commandItemViewModel,
|
||||||
WeakReference<IPageContext> errorContext,
|
WeakReference<IPageContext> errorContext,
|
||||||
DockBandSettings settings,
|
DockBandSettings settings,
|
||||||
DockSettings dockSettings,
|
ISettingsService settingsService,
|
||||||
Action saveSettings,
|
|
||||||
IContextMenuFactory contextMenuFactory)
|
IContextMenuFactory contextMenuFactory)
|
||||||
: base(errorContext)
|
: base(errorContext)
|
||||||
{
|
{
|
||||||
_rootItem = commandItemViewModel;
|
_rootItem = commandItemViewModel;
|
||||||
_bandSettings = settings;
|
_bandSettings = settings;
|
||||||
_dockSettings = dockSettings;
|
_settingsService = settingsService;
|
||||||
_saveSettings = saveSettings;
|
|
||||||
_contextMenuFactory = contextMenuFactory;
|
_contextMenuFactory = contextMenuFactory;
|
||||||
|
|
||||||
|
var dockSettings = settingsService.Settings.DockSettings;
|
||||||
_showTitles = settings.ResolveShowTitles(dockSettings.ShowLabels);
|
_showTitles = settings.ResolveShowTitles(dockSettings.ShowLabels);
|
||||||
_showSubtitles = settings.ResolveShowSubtitles(dockSettings.ShowLabels);
|
_showSubtitles = settings.ResolveShowSubtitles(dockSettings.ShowLabels);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
@@ -80,7 +81,7 @@ public sealed partial class DockViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void SetupBands(
|
private void SetupBands(
|
||||||
List<DockBandSettings> bands,
|
ImmutableList<DockBandSettings> bands,
|
||||||
ObservableCollection<DockBandViewModel> target)
|
ObservableCollection<DockBandViewModel> target)
|
||||||
{
|
{
|
||||||
List<DockBandViewModel> newBands = new();
|
List<DockBandViewModel> newBands = new();
|
||||||
@@ -148,7 +149,7 @@ public sealed partial class DockViewModel
|
|||||||
DockBandSettings bandSettings,
|
DockBandSettings bandSettings,
|
||||||
CommandItemViewModel commandItem)
|
CommandItemViewModel commandItem)
|
||||||
{
|
{
|
||||||
DockBandViewModel band = new(commandItem, commandItem.PageContext, bandSettings, _settings, SaveSettings, _contextMenuFactory);
|
DockBandViewModel band = new(commandItem, commandItem.PageContext, bandSettings, _settingsService, _contextMenuFactory);
|
||||||
|
|
||||||
// the band is NOT initialized here!
|
// the band is NOT initialized here!
|
||||||
return band;
|
return band;
|
||||||
@@ -156,7 +157,7 @@ public sealed partial class DockViewModel
|
|||||||
|
|
||||||
private void SaveSettings()
|
private void SaveSettings()
|
||||||
{
|
{
|
||||||
_settingsService.Save();
|
_settingsService.UpdateSettings(s => s with { DockSettings = _settings });
|
||||||
}
|
}
|
||||||
|
|
||||||
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
|
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
|
||||||
@@ -201,7 +202,7 @@ public sealed partial class DockViewModel
|
|||||||
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||||
{
|
{
|
||||||
var bandId = band.Id;
|
var bandId = band.Id;
|
||||||
var dockSettings = _settingsService.Settings.DockSettings;
|
var dockSettings = _settings;
|
||||||
|
|
||||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
|
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
|
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||||
@@ -213,20 +214,30 @@ public sealed partial class DockViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove from all settings lists
|
// Remove from all settings lists
|
||||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
|
var newDock = dockSettings with
|
||||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId);
|
{
|
||||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId);
|
StartBands = dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId),
|
||||||
|
CenterBands = dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId),
|
||||||
|
EndBands = dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId),
|
||||||
|
};
|
||||||
|
|
||||||
// Add to target settings list at the correct index
|
// Add to target settings list at the correct index
|
||||||
var targetSettings = targetSide switch
|
var targetList = targetSide switch
|
||||||
{
|
{
|
||||||
DockPinSide.Start => dockSettings.StartBands,
|
DockPinSide.Start => newDock.StartBands,
|
||||||
DockPinSide.Center => dockSettings.CenterBands,
|
DockPinSide.Center => newDock.CenterBands,
|
||||||
DockPinSide.End => dockSettings.EndBands,
|
DockPinSide.End => newDock.EndBands,
|
||||||
_ => dockSettings.StartBands,
|
_ => newDock.StartBands,
|
||||||
};
|
};
|
||||||
var insertIndex = Math.Min(targetIndex, targetSettings.Count);
|
var insertIndex = Math.Min(targetIndex, targetList.Count);
|
||||||
targetSettings.Insert(insertIndex, bandSettings);
|
newDock = targetSide switch
|
||||||
|
{
|
||||||
|
DockPinSide.Start => newDock with { StartBands = targetList.Insert(insertIndex, bandSettings) },
|
||||||
|
DockPinSide.Center => newDock with { CenterBands = targetList.Insert(insertIndex, bandSettings) },
|
||||||
|
DockPinSide.End => newDock with { EndBands = targetList.Insert(insertIndex, bandSettings) },
|
||||||
|
_ => newDock with { StartBands = targetList.Insert(insertIndex, bandSettings) },
|
||||||
|
};
|
||||||
|
_settings = newDock;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -236,7 +247,7 @@ public sealed partial class DockViewModel
|
|||||||
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||||
{
|
{
|
||||||
var bandId = band.Id;
|
var bandId = band.Id;
|
||||||
var dockSettings = _settingsService.Settings.DockSettings;
|
var dockSettings = _settings;
|
||||||
|
|
||||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
|
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
|
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||||
@@ -248,10 +259,15 @@ public sealed partial class DockViewModel
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from all sides (settings and UI)
|
// Remove from all sides (settings)
|
||||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
|
var newDock = dockSettings with
|
||||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId);
|
{
|
||||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId);
|
StartBands = dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId),
|
||||||
|
CenterBands = dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId),
|
||||||
|
EndBands = dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove from UI collections
|
||||||
StartItems.Remove(band);
|
StartItems.Remove(band);
|
||||||
CenterItems.Remove(band);
|
CenterItems.Remove(band);
|
||||||
EndItems.Remove(band);
|
EndItems.Remove(band);
|
||||||
@@ -261,8 +277,8 @@ public sealed partial class DockViewModel
|
|||||||
{
|
{
|
||||||
case DockPinSide.Start:
|
case DockPinSide.Start:
|
||||||
{
|
{
|
||||||
var settingsIndex = Math.Min(targetIndex, dockSettings.StartBands.Count);
|
var settingsIndex = Math.Min(targetIndex, newDock.StartBands.Count);
|
||||||
dockSettings.StartBands.Insert(settingsIndex, bandSettings);
|
newDock = newDock with { StartBands = newDock.StartBands.Insert(settingsIndex, bandSettings) };
|
||||||
|
|
||||||
var uiIndex = Math.Min(targetIndex, StartItems.Count);
|
var uiIndex = Math.Min(targetIndex, StartItems.Count);
|
||||||
StartItems.Insert(uiIndex, band);
|
StartItems.Insert(uiIndex, band);
|
||||||
@@ -271,8 +287,8 @@ public sealed partial class DockViewModel
|
|||||||
|
|
||||||
case DockPinSide.Center:
|
case DockPinSide.Center:
|
||||||
{
|
{
|
||||||
var settingsIndex = Math.Min(targetIndex, dockSettings.CenterBands.Count);
|
var settingsIndex = Math.Min(targetIndex, newDock.CenterBands.Count);
|
||||||
dockSettings.CenterBands.Insert(settingsIndex, bandSettings);
|
newDock = newDock with { CenterBands = newDock.CenterBands.Insert(settingsIndex, bandSettings) };
|
||||||
|
|
||||||
var uiIndex = Math.Min(targetIndex, CenterItems.Count);
|
var uiIndex = Math.Min(targetIndex, CenterItems.Count);
|
||||||
CenterItems.Insert(uiIndex, band);
|
CenterItems.Insert(uiIndex, band);
|
||||||
@@ -281,8 +297,8 @@ public sealed partial class DockViewModel
|
|||||||
|
|
||||||
case DockPinSide.End:
|
case DockPinSide.End:
|
||||||
{
|
{
|
||||||
var settingsIndex = Math.Min(targetIndex, dockSettings.EndBands.Count);
|
var settingsIndex = Math.Min(targetIndex, newDock.EndBands.Count);
|
||||||
dockSettings.EndBands.Insert(settingsIndex, bandSettings);
|
newDock = newDock with { EndBands = newDock.EndBands.Insert(settingsIndex, bandSettings) };
|
||||||
|
|
||||||
var uiIndex = Math.Min(targetIndex, EndItems.Count);
|
var uiIndex = Math.Min(targetIndex, EndItems.Count);
|
||||||
EndItems.Insert(uiIndex, band);
|
EndItems.Insert(uiIndex, band);
|
||||||
@@ -290,6 +306,8 @@ public sealed partial class DockViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_settings = newDock;
|
||||||
|
|
||||||
Logger.LogDebug($"Moved band {bandId} to {targetSide} at index {targetIndex} (not saved yet)");
|
Logger.LogDebug($"Moved band {bandId} to {targetSide} at index {targetIndex} (not saved yet)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,22 +323,57 @@ public sealed partial class DockViewModel
|
|||||||
band.SaveShowLabels();
|
band.SaveShowLabels();
|
||||||
}
|
}
|
||||||
|
|
||||||
_snapshotStartBands = null;
|
// Preserve any per-band label edits made while in edit mode. Those edits are
|
||||||
_snapshotCenterBands = null;
|
// saved independently of reorder, so merge the latest band settings back into
|
||||||
_snapshotEndBands = null;
|
// the local reordered snapshot before we persist dock settings.
|
||||||
|
var latestBandSettings = BuildBandSettingsLookup(_settingsService.Settings.DockSettings);
|
||||||
|
_settings = _settings with
|
||||||
|
{
|
||||||
|
StartBands = MergeBandSettings(_settings.StartBands, latestBandSettings),
|
||||||
|
CenterBands = MergeBandSettings(_settings.CenterBands, latestBandSettings),
|
||||||
|
EndBands = MergeBandSettings(_settings.EndBands, latestBandSettings),
|
||||||
|
};
|
||||||
|
|
||||||
|
_snapshotDockSettings = null;
|
||||||
_snapshotBandViewModels = null;
|
_snapshotBandViewModels = null;
|
||||||
|
|
||||||
// Save without hotReload to avoid triggering SettingsChanged → SetupBands,
|
// Save without hotReload to avoid triggering SettingsChanged → SetupBands,
|
||||||
// which could race with stale DockBands_CollectionChanged work items and
|
// which could race with stale DockBands_CollectionChanged work items and
|
||||||
// re-add bands that were just unpinned.
|
// re-add bands that were just unpinned.
|
||||||
_settingsService.Save(hotReload: false);
|
_settingsService.UpdateSettings(s => s with { DockSettings = _settings }, false);
|
||||||
_isEditing = false;
|
_isEditing = false;
|
||||||
Logger.LogDebug("Saved band order to settings");
|
Logger.LogDebug("Saved band order to settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DockBandSettings>? _snapshotStartBands;
|
private static Dictionary<string, DockBandSettings> BuildBandSettingsLookup(DockSettings dockSettings)
|
||||||
private List<DockBandSettings>? _snapshotCenterBands;
|
{
|
||||||
private List<DockBandSettings>? _snapshotEndBands;
|
var lookup = new Dictionary<string, DockBandSettings>(StringComparer.Ordinal);
|
||||||
|
foreach (var band in dockSettings.StartBands.Concat(dockSettings.CenterBands).Concat(dockSettings.EndBands))
|
||||||
|
{
|
||||||
|
lookup[band.CommandId] = band;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableList<DockBandSettings> MergeBandSettings(
|
||||||
|
ImmutableList<DockBandSettings> targetBands,
|
||||||
|
IReadOnlyDictionary<string, DockBandSettings> latestBandSettings)
|
||||||
|
{
|
||||||
|
var merged = targetBands;
|
||||||
|
for (var i = 0; i < merged.Count; i++)
|
||||||
|
{
|
||||||
|
var commandId = merged[i].CommandId;
|
||||||
|
if (latestBandSettings.TryGetValue(commandId, out var latestSettings))
|
||||||
|
{
|
||||||
|
merged = merged.SetItem(i, latestSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DockSettings? _snapshotDockSettings;
|
||||||
private Dictionary<string, DockBandViewModel>? _snapshotBandViewModels;
|
private Dictionary<string, DockBandViewModel>? _snapshotBandViewModels;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -332,12 +385,14 @@ public sealed partial class DockViewModel
|
|||||||
_isEditing = true;
|
_isEditing = true;
|
||||||
|
|
||||||
var dockSettings = _settingsService.Settings.DockSettings;
|
var dockSettings = _settingsService.Settings.DockSettings;
|
||||||
_snapshotStartBands = dockSettings.StartBands.Select(b => b.Clone()).ToList();
|
|
||||||
_snapshotCenterBands = dockSettings.CenterBands.Select(b => b.Clone()).ToList();
|
var snapshotStartBandsCount = dockSettings.StartBands.Count;
|
||||||
_snapshotEndBands = dockSettings.EndBands.Select(b => b.Clone()).ToList();
|
var snapshotCenterBandsCount = dockSettings.CenterBands.Count;
|
||||||
|
var snapshotEndBandsCount = dockSettings.EndBands.Count;
|
||||||
|
|
||||||
// Snapshot band ViewModels so we can restore unpinned bands
|
// Snapshot band ViewModels so we can restore unpinned bands
|
||||||
// Use a dictionary but handle potential duplicates gracefully
|
// Use a dictionary but handle potential duplicates gracefully
|
||||||
|
_snapshotDockSettings = dockSettings;
|
||||||
_snapshotBandViewModels = new Dictionary<string, DockBandViewModel>();
|
_snapshotBandViewModels = new Dictionary<string, DockBandViewModel>();
|
||||||
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
||||||
{
|
{
|
||||||
@@ -350,7 +405,7 @@ public sealed partial class DockViewModel
|
|||||||
band.SnapshotShowLabels();
|
band.SnapshotShowLabels();
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogDebug($"Snapshot taken: {_snapshotStartBands.Count} start bands, {_snapshotCenterBands.Count} center bands, {_snapshotEndBands.Count} end bands");
|
Logger.LogDebug($"Snapshot taken: {snapshotStartBandsCount} start bands, {snapshotCenterBandsCount} center bands, {snapshotEndBandsCount} end bands");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -359,9 +414,7 @@ public sealed partial class DockViewModel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void RestoreBandOrder()
|
public void RestoreBandOrder()
|
||||||
{
|
{
|
||||||
if (_snapshotStartBands == null ||
|
if (_snapshotDockSettings == null || _snapshotBandViewModels == null)
|
||||||
_snapshotCenterBands == null ||
|
|
||||||
_snapshotEndBands == null || _snapshotBandViewModels == null)
|
|
||||||
{
|
{
|
||||||
Logger.LogWarning("No snapshot to restore from");
|
Logger.LogWarning("No snapshot to restore from");
|
||||||
return;
|
return;
|
||||||
@@ -373,37 +426,13 @@ public sealed partial class DockViewModel
|
|||||||
band.RestoreShowLabels();
|
band.RestoreShowLabels();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dockSettings = _settingsService.Settings.DockSettings;
|
// Restore settings from snapshot (immutable = just assign back)
|
||||||
|
_settings = _snapshotDockSettings;
|
||||||
// Restore settings from snapshot
|
|
||||||
dockSettings.StartBands.Clear();
|
|
||||||
dockSettings.CenterBands.Clear();
|
|
||||||
dockSettings.EndBands.Clear();
|
|
||||||
|
|
||||||
foreach (var bandSnapshot in _snapshotStartBands)
|
|
||||||
{
|
|
||||||
var bandSettings = bandSnapshot.Clone();
|
|
||||||
dockSettings.StartBands.Add(bandSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var bandSnapshot in _snapshotCenterBands)
|
|
||||||
{
|
|
||||||
var bandSettings = bandSnapshot.Clone();
|
|
||||||
dockSettings.CenterBands.Add(bandSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var bandSnapshot in _snapshotEndBands)
|
|
||||||
{
|
|
||||||
var bandSettings = bandSnapshot.Clone();
|
|
||||||
dockSettings.EndBands.Add(bandSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebuild UI collections from restored settings using the snapshotted ViewModels
|
// Rebuild UI collections from restored settings using the snapshotted ViewModels
|
||||||
RebuildUICollectionsFromSnapshot();
|
RebuildUICollectionsFromSnapshot();
|
||||||
|
|
||||||
_snapshotStartBands = null;
|
_snapshotDockSettings = null;
|
||||||
_snapshotCenterBands = null;
|
|
||||||
_snapshotEndBands = null;
|
|
||||||
_snapshotBandViewModels = null;
|
_snapshotBandViewModels = null;
|
||||||
_isEditing = false;
|
_isEditing = false;
|
||||||
Logger.LogDebug("Restored band order from snapshot");
|
Logger.LogDebug("Restored band order from snapshot");
|
||||||
@@ -416,7 +445,7 @@ public sealed partial class DockViewModel
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var dockSettings = _settingsService.Settings.DockSettings;
|
var dockSettings = _settings;
|
||||||
|
|
||||||
StartItems.Clear();
|
StartItems.Clear();
|
||||||
CenterItems.Clear();
|
CenterItems.Clear();
|
||||||
@@ -449,7 +478,7 @@ public sealed partial class DockViewModel
|
|||||||
|
|
||||||
private void RebuildUICollections()
|
private void RebuildUICollections()
|
||||||
{
|
{
|
||||||
var dockSettings = _settingsService.Settings.DockSettings;
|
var dockSettings = _settings;
|
||||||
|
|
||||||
// Create a lookup of all current band ViewModels
|
// Create a lookup of all current band ViewModels
|
||||||
var allBands = StartItems.Concat(CenterItems).Concat(EndItems).ToDictionary(b => b.Id);
|
var allBands = StartItems.Concat(CenterItems).Concat(EndItems).ToDictionary(b => b.Id);
|
||||||
@@ -526,7 +555,7 @@ public sealed partial class DockViewModel
|
|||||||
|
|
||||||
// Create settings for the new band
|
// Create settings for the new band
|
||||||
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId, ShowLabels = null };
|
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId, ShowLabels = null };
|
||||||
var dockSettings = _settingsService.Settings.DockSettings;
|
var dockSettings = _settings;
|
||||||
|
|
||||||
// Create the band view model
|
// Create the band view model
|
||||||
var bandVm = CreateBandItem(bandSettings, topLevel.ItemViewModel);
|
var bandVm = CreateBandItem(bandSettings, topLevel.ItemViewModel);
|
||||||
@@ -535,15 +564,15 @@ public sealed partial class DockViewModel
|
|||||||
switch (targetSide)
|
switch (targetSide)
|
||||||
{
|
{
|
||||||
case DockPinSide.Start:
|
case DockPinSide.Start:
|
||||||
dockSettings.StartBands.Add(bandSettings);
|
_settings = dockSettings with { StartBands = dockSettings.StartBands.Add(bandSettings) };
|
||||||
StartItems.Add(bandVm);
|
StartItems.Add(bandVm);
|
||||||
break;
|
break;
|
||||||
case DockPinSide.Center:
|
case DockPinSide.Center:
|
||||||
dockSettings.CenterBands.Add(bandSettings);
|
_settings = dockSettings with { CenterBands = dockSettings.CenterBands.Add(bandSettings) };
|
||||||
CenterItems.Add(bandVm);
|
CenterItems.Add(bandVm);
|
||||||
break;
|
break;
|
||||||
case DockPinSide.End:
|
case DockPinSide.End:
|
||||||
dockSettings.EndBands.Add(bandSettings);
|
_settings = dockSettings with { EndBands = dockSettings.EndBands.Add(bandSettings) };
|
||||||
EndItems.Add(bandVm);
|
EndItems.Add(bandVm);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -566,12 +595,15 @@ public sealed partial class DockViewModel
|
|||||||
public void UnpinBand(DockBandViewModel band)
|
public void UnpinBand(DockBandViewModel band)
|
||||||
{
|
{
|
||||||
var bandId = band.Id;
|
var bandId = band.Id;
|
||||||
var dockSettings = _settingsService.Settings.DockSettings;
|
var dockSettings = _settings;
|
||||||
|
|
||||||
// Remove from settings
|
// Remove from settings
|
||||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
|
_settings = dockSettings with
|
||||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId);
|
{
|
||||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId);
|
StartBands = dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId),
|
||||||
|
CenterBands = dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId),
|
||||||
|
EndBands = dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId),
|
||||||
|
};
|
||||||
|
|
||||||
// Remove from UI collections
|
// Remove from UI collections
|
||||||
StartItems.Remove(band);
|
StartItems.Remove(band);
|
||||||
@@ -635,7 +667,7 @@ public sealed partial class DockViewModel
|
|||||||
var isDockEnabled = _settingsService.Settings.EnableDock;
|
var isDockEnabled = _settingsService.Settings.EnableDock;
|
||||||
var dockSide = isDockEnabled ? _settings.Side.ToString().ToLowerInvariant() : "none";
|
var dockSide = isDockEnabled ? _settings.Side.ToString().ToLowerInvariant() : "none";
|
||||||
|
|
||||||
static string FormatBands(List<DockBandSettings> bands) =>
|
static string FormatBands(ImmutableList<DockBandSettings> bands) =>
|
||||||
string.Join("\n", bands.Select(b => $"{b.ProviderId}/{b.CommandId}"));
|
string.Join("\n", bands.Select(b => $"{b.ProviderId}/{b.CommandId}"));
|
||||||
|
|
||||||
var startBands = isDockEnabled ? FormatBands(_settings.StartBands) : string.Empty;
|
var startBands = isDockEnabled ? FormatBands(_settings.StartBands) : string.Empty;
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.DockSettings.Theme != value)
|
if (_settingsService.Settings.DockSettings.Theme != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.Theme = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { Theme = value } });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(ThemeIndex));
|
OnPropertyChanged(nameof(ThemeIndex));
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,10 +68,10 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.DockSettings.Backdrop != value)
|
if (_settingsService.Settings.DockSettings.Backdrop != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.Backdrop = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { Backdrop = value } });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(BackdropIndex));
|
OnPropertyChanged(nameof(BackdropIndex));
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.DockSettings.ColorizationMode != value)
|
if (_settingsService.Settings.DockSettings.ColorizationMode != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.ColorizationMode = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { ColorizationMode = value } });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||||
@@ -99,7 +99,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
|
|
||||||
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
||||||
|
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.DockSettings.CustomThemeColor != value)
|
if (_settingsService.Settings.DockSettings.CustomThemeColor != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.CustomThemeColor = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { CustomThemeColor = value } });
|
||||||
|
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
ColorIntensity = 100;
|
ColorIntensity = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,9 +136,9 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
get => _settingsService.Settings.DockSettings.CustomThemeColorIntensity;
|
get => _settingsService.Settings.DockSettings.CustomThemeColorIntensity;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.CustomThemeColorIntensity = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { CustomThemeColorIntensity = value } });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.DockSettings.BackgroundImagePath != value)
|
if (_settingsService.Settings.DockSettings.BackgroundImagePath != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.BackgroundImagePath = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { BackgroundImagePath = value } });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
|
||||||
if (BackgroundImageOpacity == 0)
|
if (BackgroundImageOpacity == 0)
|
||||||
@@ -157,7 +157,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
BackgroundImageOpacity = 100;
|
BackgroundImageOpacity = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,9 +169,9 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.DockSettings.BackgroundImageOpacity != value)
|
if (_settingsService.Settings.DockSettings.BackgroundImageOpacity != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.BackgroundImageOpacity = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { BackgroundImageOpacity = value } });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,9 +183,9 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.DockSettings.BackgroundImageBrightness != value)
|
if (_settingsService.Settings.DockSettings.BackgroundImageBrightness != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.BackgroundImageBrightness = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { BackgroundImageBrightness = value } });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,9 +197,9 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.DockSettings.BackgroundImageBlurAmount != value)
|
if (_settingsService.Settings.DockSettings.BackgroundImageBlurAmount != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.BackgroundImageBlurAmount = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { BackgroundImageBlurAmount = value } });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,10 +211,10 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
{
|
{
|
||||||
if (_settingsService.Settings.DockSettings.BackgroundImageFit != value)
|
if (_settingsService.Settings.DockSettings.BackgroundImageFit != value)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.BackgroundImageFit = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { BackgroundImageFit = value } });
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||||
Save();
|
DebouncedReapply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,9 +298,8 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
|||||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Save()
|
private void DebouncedReapply()
|
||||||
{
|
{
|
||||||
_settingsService.Save();
|
|
||||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
public class FallbackSettings
|
public record FallbackSettings
|
||||||
{
|
{
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool IsEnabled { get; init; } = true;
|
||||||
|
|
||||||
public bool IncludeInGlobalResults { get; set; }
|
public bool IncludeInGlobalResults { get; init; }
|
||||||
|
|
||||||
public FallbackSettings()
|
public FallbackSettings()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
|||||||
public partial class FallbackSettingsViewModel : ObservableObject
|
public partial class FallbackSettingsViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly FallbackSettings _fallbackSettings;
|
private readonly ProviderSettingsViewModel _providerSettingsViewModel;
|
||||||
|
|
||||||
|
private FallbackSettings _fallbackSettings;
|
||||||
|
|
||||||
public string DisplayName { get; private set; } = string.Empty;
|
public string DisplayName { get; private set; } = string.Empty;
|
||||||
|
|
||||||
@@ -27,15 +29,18 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
if (value != _fallbackSettings.IsEnabled)
|
if (value != _fallbackSettings.IsEnabled)
|
||||||
{
|
{
|
||||||
_fallbackSettings.IsEnabled = value;
|
var newSettings = _fallbackSettings with { IsEnabled = value };
|
||||||
|
|
||||||
if (!_fallbackSettings.IsEnabled)
|
if (!newSettings.IsEnabled)
|
||||||
{
|
{
|
||||||
_fallbackSettings.IncludeInGlobalResults = false;
|
newSettings = newSettings with { IncludeInGlobalResults = false };
|
||||||
}
|
}
|
||||||
|
|
||||||
Save();
|
_fallbackSettings = newSettings;
|
||||||
|
_providerSettingsViewModel.UpdateFallbackSettings(Id, _fallbackSettings);
|
||||||
|
|
||||||
OnPropertyChanged(nameof(IsEnabled));
|
OnPropertyChanged(nameof(IsEnabled));
|
||||||
|
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,15 +52,18 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
if (value != _fallbackSettings.IncludeInGlobalResults)
|
if (value != _fallbackSettings.IncludeInGlobalResults)
|
||||||
{
|
{
|
||||||
_fallbackSettings.IncludeInGlobalResults = value;
|
var newSettings = _fallbackSettings with { IncludeInGlobalResults = value };
|
||||||
|
|
||||||
if (!_fallbackSettings.IsEnabled)
|
if (!newSettings.IsEnabled)
|
||||||
{
|
{
|
||||||
_fallbackSettings.IsEnabled = true;
|
newSettings = newSettings with { IsEnabled = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
Save();
|
_fallbackSettings = newSettings;
|
||||||
|
_providerSettingsViewModel.UpdateFallbackSettings(Id, _fallbackSettings);
|
||||||
|
|
||||||
OnPropertyChanged(nameof(IncludeInGlobalResults));
|
OnPropertyChanged(nameof(IncludeInGlobalResults));
|
||||||
|
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,6 +75,7 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
|||||||
ISettingsService settingsService)
|
ISettingsService settingsService)
|
||||||
{
|
{
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
|
_providerSettingsViewModel = providerSettings;
|
||||||
_fallbackSettings = fallbackSettings;
|
_fallbackSettings = fallbackSettings;
|
||||||
|
|
||||||
Id = fallback.Id;
|
Id = fallback.Id;
|
||||||
@@ -77,10 +86,4 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
|||||||
Icon = new(fallback.InitialIcon);
|
Icon = new(fallback.InitialIcon);
|
||||||
Icon.InitializeProperties();
|
Icon.InitializeProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Save()
|
|
||||||
{
|
|
||||||
_settingsService.Save();
|
|
||||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
|||||||
|
|
||||||
public record HistoryItem
|
public record HistoryItem
|
||||||
{
|
{
|
||||||
public required string CommandId { get; set; }
|
public required string CommandId { get; init; }
|
||||||
|
|
||||||
public required int Uses { get; set; }
|
public required int Uses { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,36 +11,28 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
|||||||
public partial class HotkeyManager : ObservableObject
|
public partial class HotkeyManager : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||||
private readonly List<TopLevelHotkey> _commandHotkeys;
|
private readonly ISettingsService _settingsService;
|
||||||
|
|
||||||
public HotkeyManager(TopLevelCommandManager tlcManager, ISettingsService settingsService)
|
public HotkeyManager(TopLevelCommandManager tlcManager, ISettingsService settingsService)
|
||||||
{
|
{
|
||||||
_topLevelCommandManager = tlcManager;
|
_topLevelCommandManager = tlcManager;
|
||||||
_commandHotkeys = settingsService.Settings.CommandHotkeys;
|
_settingsService = settingsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateHotkey(string commandId, HotkeySettings? hotkey)
|
public void UpdateHotkey(string commandId, HotkeySettings? hotkey)
|
||||||
{
|
{
|
||||||
// If any of the commands were already bound to this hotkey, remove that
|
_settingsService.UpdateSettings(s =>
|
||||||
foreach (var item in _commandHotkeys)
|
|
||||||
{
|
{
|
||||||
if (item.Hotkey == hotkey)
|
// Remove any command already bound to this hotkey, and remove old binding for this command
|
||||||
|
var hotkeys = s.CommandHotkeys
|
||||||
|
.RemoveAll(item => item.Hotkey == hotkey || item.CommandId == commandId);
|
||||||
|
|
||||||
|
if (hotkey is not null)
|
||||||
{
|
{
|
||||||
item.Hotkey = null;
|
hotkeys = hotkeys.Add(new(hotkey, commandId));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_commandHotkeys.RemoveAll(item => item.Hotkey is null);
|
return s with { CommandHotkeys = hotkeys };
|
||||||
|
});
|
||||||
foreach (var item in _commandHotkeys)
|
|
||||||
{
|
|
||||||
if (item.CommandId == commandId)
|
|
||||||
{
|
|
||||||
_commandHotkeys.Remove(item);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_commandHotkeys.Add(new(hotkey, commandId));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,37 +2,39 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
public class ProviderSettings
|
public record ProviderSettings
|
||||||
{
|
{
|
||||||
// List of built-in fallbacks that should not have global results enabled by default
|
// List of built-in fallbacks that should not have global results enabled by default
|
||||||
private readonly string[] _excludedBuiltInFallbacks = [
|
private static readonly string[] _excludedBuiltInFallbacks = [
|
||||||
"com.microsoft.cmdpal.builtin.indexer.fallback",
|
"com.microsoft.cmdpal.builtin.indexer.fallback",
|
||||||
"com.microsoft.cmdpal.builtin.calculator.fallback",
|
"com.microsoft.cmdpal.builtin.calculator.fallback",
|
||||||
"com.microsoft.cmdpal.builtin.remotedesktop.fallback",
|
"com.microsoft.cmdpal.builtin.remotedesktop.fallback",
|
||||||
];
|
];
|
||||||
|
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool IsEnabled { get; init; } = true;
|
||||||
|
|
||||||
public Dictionary<string, FallbackSettings> FallbackCommands { get; set; } = new();
|
public ImmutableDictionary<string, FallbackSettings> FallbackCommands { get; init; }
|
||||||
|
= ImmutableDictionary<string, FallbackSettings>.Empty;
|
||||||
|
|
||||||
public List<string> PinnedCommandIds { get; set; } = [];
|
public ImmutableList<string> PinnedCommandIds { get; init; }
|
||||||
|
= ImmutableList<string>.Empty;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string ProviderDisplayName { get; set; } = string.Empty;
|
public string ProviderId { get; init; } = string.Empty;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string ProviderId { get; private set; } = string.Empty;
|
public bool IsBuiltin { get; init; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsBuiltin { get; private set; }
|
public string ProviderDisplayName { get; init; } = string.Empty;
|
||||||
|
|
||||||
public ProviderSettings(CommandProviderWrapper wrapper)
|
public ProviderSettings()
|
||||||
{
|
{
|
||||||
Connect(wrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
@@ -41,28 +43,51 @@ public class ProviderSettings
|
|||||||
IsEnabled = isEnabled;
|
IsEnabled = isEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Connect(CommandProviderWrapper wrapper)
|
/// <summary>
|
||||||
|
/// Returns a new ProviderSettings connected to the given wrapper.
|
||||||
|
/// Returns <see langword="this"/> when the connection produces no changes.
|
||||||
|
/// Pure function — does not mutate this instance.
|
||||||
|
/// </summary>
|
||||||
|
public ProviderSettings WithConnection(CommandProviderWrapper wrapper)
|
||||||
{
|
{
|
||||||
ProviderId = wrapper.ProviderId;
|
if (string.IsNullOrWhiteSpace(wrapper.ProviderId))
|
||||||
IsBuiltin = wrapper.Extension is null;
|
{
|
||||||
|
throw new ArgumentException("ProviderId must not be null, empty, or whitespace.", nameof(wrapper));
|
||||||
ProviderDisplayName = wrapper.DisplayName;
|
}
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
|
var builder = FallbackCommands.ToBuilder();
|
||||||
if (wrapper.FallbackItems.Length > 0)
|
if (wrapper.FallbackItems.Length > 0)
|
||||||
{
|
{
|
||||||
foreach (var fallback in wrapper.FallbackItems)
|
foreach (var fallback in wrapper.FallbackItems)
|
||||||
{
|
{
|
||||||
if (!FallbackCommands.ContainsKey(fallback.Id))
|
if (!string.IsNullOrEmpty(fallback.Id) && !builder.ContainsKey(fallback.Id))
|
||||||
{
|
{
|
||||||
var enableGlobalResults = IsBuiltin && !_excludedBuiltInFallbacks.Contains(fallback.Id);
|
var enableGlobalResults = (wrapper.Extension is null)
|
||||||
FallbackCommands[fallback.Id] = new FallbackSettings(enableGlobalResults);
|
&& !_excludedBuiltInFallbacks.Contains(fallback.Id);
|
||||||
|
builder[fallback.Id] = new FallbackSettings(enableGlobalResults);
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(ProviderId))
|
var isBuiltin = wrapper.Extension is null;
|
||||||
|
|
||||||
|
// If nothing changed, return the same instance to avoid unnecessary allocations and saves
|
||||||
|
if (!changed
|
||||||
|
&& ProviderId == wrapper.ProviderId
|
||||||
|
&& IsBuiltin == isBuiltin
|
||||||
|
&& ProviderDisplayName == wrapper.DisplayName)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException("Did you add a built-in command and forget to set the Id? Make sure you do that!");
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this with
|
||||||
|
{
|
||||||
|
ProviderId = wrapper.ProviderId,
|
||||||
|
IsBuiltin = isBuiltin,
|
||||||
|
ProviderDisplayName = wrapper.DisplayName,
|
||||||
|
FallbackCommands = changed ? builder.ToImmutable() : FallbackCommands,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ public partial class ProviderSettingsViewModel : ObservableObject
|
|||||||
private static readonly CompositeFormat ExtensionSubtextDisabledFormat = CompositeFormat.Parse(Resources.builtin_extension_subtext_disabled);
|
private static readonly CompositeFormat ExtensionSubtextDisabledFormat = CompositeFormat.Parse(Resources.builtin_extension_subtext_disabled);
|
||||||
|
|
||||||
private readonly CommandProviderWrapper _provider;
|
private readonly CommandProviderWrapper _provider;
|
||||||
private readonly ProviderSettings _providerSettings;
|
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly Lock _initializeSettingsLock = new();
|
private readonly Lock _initializeSettingsLock = new();
|
||||||
|
|
||||||
|
private ProviderSettings _providerSettings;
|
||||||
|
|
||||||
private Task? _initializeSettingsTask;
|
private Task? _initializeSettingsTask;
|
||||||
|
|
||||||
public ProviderSettingsViewModel(
|
public ProviderSettingsViewModel(
|
||||||
@@ -71,8 +72,13 @@ public partial class ProviderSettingsViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
if (value != _providerSettings.IsEnabled)
|
if (value != _providerSettings.IsEnabled)
|
||||||
{
|
{
|
||||||
_providerSettings.IsEnabled = value;
|
var newSettings = _providerSettings with { IsEnabled = value };
|
||||||
Save();
|
_settingsService.UpdateSettings(s => s with
|
||||||
|
{
|
||||||
|
|
||||||
|
ProviderSettings = s.ProviderSettings.SetItem(_provider.ProviderId, newSettings),
|
||||||
|
});
|
||||||
|
_providerSettings = newSettings;
|
||||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||||
OnPropertyChanged(nameof(IsEnabled));
|
OnPropertyChanged(nameof(IsEnabled));
|
||||||
OnPropertyChanged(nameof(ExtensionSubtext));
|
OnPropertyChanged(nameof(ExtensionSubtext));
|
||||||
@@ -191,7 +197,20 @@ public partial class ProviderSettingsViewModel : ObservableObject
|
|||||||
FallbackCommands = fallbackViewModels;
|
FallbackCommands = fallbackViewModels;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Save() => _settingsService.Save();
|
internal void UpdateFallbackSettings(string id, FallbackSettings settings)
|
||||||
|
{
|
||||||
|
var newProviderSettings = _providerSettings with
|
||||||
|
{
|
||||||
|
FallbackCommands = _providerSettings.FallbackCommands.SetItem(id, settings),
|
||||||
|
};
|
||||||
|
_providerSettings = newProviderSettings;
|
||||||
|
_settingsService.UpdateSettings(
|
||||||
|
s => s with
|
||||||
|
{
|
||||||
|
ProviderSettings = s.ProviderSettings.SetItem(_provider.ProviderId, newProviderSettings),
|
||||||
|
},
|
||||||
|
hotReload: false);
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeSettingsPage()
|
private void InitializeSettingsPage()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,17 +2,15 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
public partial class RecentCommandsManager : ObservableObject, IRecentCommandsManager
|
public record RecentCommandsManager : IRecentCommandsManager
|
||||||
{
|
{
|
||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
internal List<HistoryItem> History { get; set; } = [];
|
internal ImmutableList<HistoryItem> History { get; init; } = ImmutableList<HistoryItem>.Empty;
|
||||||
|
|
||||||
private readonly Lock _lock = new();
|
|
||||||
|
|
||||||
public RecentCommandsManager()
|
public RecentCommandsManager()
|
||||||
{
|
{
|
||||||
@@ -20,64 +18,64 @@ public partial class RecentCommandsManager : ObservableObject, IRecentCommandsMa
|
|||||||
|
|
||||||
public int GetCommandHistoryWeight(string commandId)
|
public int GetCommandHistoryWeight(string commandId)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
var entry = History
|
||||||
{
|
|
||||||
var entry = History
|
|
||||||
.Index()
|
.Index()
|
||||||
.Where(item => item.Item.CommandId == commandId)
|
.Where(item => item.Item.CommandId == commandId)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
// These numbers are vaguely scaled so that "VS" will make "Visual Studio" the
|
// These numbers are vaguely scaled so that "VS" will make "Visual Studio" the
|
||||||
// match after one use.
|
// match after one use.
|
||||||
// Usually it has a weight of 84, compared to 109 for the VS cmd prompt
|
// Usually it has a weight of 84, compared to 109 for the VS cmd prompt
|
||||||
if (entry.Item is not null)
|
if (entry.Item is not null)
|
||||||
|
{
|
||||||
|
var index = entry.Index;
|
||||||
|
|
||||||
|
// First, add some weight based on how early in the list this appears
|
||||||
|
var bucket = index switch
|
||||||
{
|
{
|
||||||
var index = entry.Index;
|
_ when index <= 2 => 35,
|
||||||
|
_ when index <= 10 => 25,
|
||||||
|
_ when index <= 15 => 15,
|
||||||
|
_ when index <= 35 => 10,
|
||||||
|
_ => 5,
|
||||||
|
};
|
||||||
|
|
||||||
// First, add some weight based on how early in the list this appears
|
// Then, add weight for how often this is used, but cap the weight from usage.
|
||||||
var bucket = index switch
|
var uses = Math.Min(entry.Item.Uses * 5, 35);
|
||||||
{
|
|
||||||
var i when index <= 2 => 35,
|
|
||||||
var i when index <= 10 => 25,
|
|
||||||
var i when index <= 15 => 15,
|
|
||||||
var i when index <= 35 => 10,
|
|
||||||
_ => 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Then, add weight for how often this is used, but cap the weight from usage.
|
return bucket + uses;
|
||||||
var uses = Math.Min(entry.Item.Uses * 5, 35);
|
|
||||||
|
|
||||||
return bucket + uses;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddHistoryItem(string commandId)
|
/// <summary>
|
||||||
|
/// Returns a new RecentCommandsManager with the given command added/promoted in history.
|
||||||
|
/// Pure function — does not mutate this instance.
|
||||||
|
/// </summary>
|
||||||
|
public RecentCommandsManager WithHistoryItem(string commandId)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
var existing = History.FirstOrDefault(item => item.CommandId == commandId);
|
||||||
{
|
ImmutableList<HistoryItem> newHistory;
|
||||||
var entry = History
|
|
||||||
.Where(item => item.CommandId == commandId)
|
|
||||||
.FirstOrDefault();
|
|
||||||
if (entry is null)
|
|
||||||
{
|
|
||||||
var newitem = new HistoryItem() { CommandId = commandId, Uses = 1 };
|
|
||||||
History.Insert(0, newitem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
History.Remove(entry);
|
|
||||||
entry.Uses++;
|
|
||||||
History.Insert(0, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (History.Count > 50)
|
if (existing is not null)
|
||||||
{
|
{
|
||||||
History.RemoveRange(50, History.Count - 50);
|
newHistory = History.Remove(existing);
|
||||||
}
|
var updated = existing with { Uses = existing.Uses + 1 };
|
||||||
|
newHistory = newHistory.Insert(0, updated);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var newItem = new HistoryItem { CommandId = commandId, Uses = 1 };
|
||||||
|
newHistory = History.Insert(0, newItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newHistory.Count > 50)
|
||||||
|
{
|
||||||
|
newHistory = newHistory.RemoveRange(50, newHistory.Count - 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this with { History = newHistory };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,5 +83,5 @@ public interface IRecentCommandsManager
|
|||||||
{
|
{
|
||||||
int GetCommandHistoryWeight(string commandId);
|
int GetCommandHistoryWeight(string commandId);
|
||||||
|
|
||||||
void AddHistoryItem(string commandId);
|
RecentCommandsManager WithHistoryItem(string commandId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,20 +22,35 @@ public sealed class AppStateService : IAppStateService
|
|||||||
_persistence = persistence;
|
_persistence = persistence;
|
||||||
_appInfoService = appInfoService;
|
_appInfoService = appInfoService;
|
||||||
_filePath = StateJsonPath();
|
_filePath = StateJsonPath();
|
||||||
State = _persistence.Load(_filePath, JsonSerializationContext.Default.AppStateModel);
|
_state = _persistence.Load(_filePath, JsonSerializationContext.Default.AppStateModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AppStateModel _state;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public AppStateModel State { get; private set; }
|
public AppStateModel State => Volatile.Read(ref _state);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public event TypedEventHandler<IAppStateService, AppStateModel>? StateChanged;
|
public event TypedEventHandler<IAppStateService, AppStateModel>? StateChanged;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Save()
|
public void Save() => UpdateState(s => s);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void UpdateState(Func<AppStateModel, AppStateModel> transform)
|
||||||
{
|
{
|
||||||
_persistence.Save(State, _filePath, JsonSerializationContext.Default.AppStateModel);
|
AppStateModel snapshot;
|
||||||
StateChanged?.Invoke(this, State);
|
AppStateModel updated;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
snapshot = Volatile.Read(ref _state);
|
||||||
|
updated = transform(snapshot);
|
||||||
|
}
|
||||||
|
while (Interlocked.CompareExchange(ref _state, updated, snapshot) != snapshot);
|
||||||
|
|
||||||
|
var newState = Volatile.Read(ref _state);
|
||||||
|
_persistence.Save(newState, _filePath, JsonSerializationContext.Default.AppStateModel);
|
||||||
|
StateChanged?.Invoke(this, newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string StateJsonPath()
|
private string StateJsonPath()
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ public interface IAppStateService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
void Save();
|
void Save();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atomically applies a transformation to the current state, persists the result,
|
||||||
|
/// and raises <see cref="StateChanged"/>.
|
||||||
|
/// </summary>
|
||||||
|
void UpdateState(Func<AppStateModel, AppStateModel> transform);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raised after state has been saved to disk.
|
/// Raised after state has been saved to disk.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ public interface ISettingsService
|
|||||||
/// <param name="hotReload">When <see langword="true"/>, raises <see cref="SettingsChanged"/> after saving.</param>
|
/// <param name="hotReload">When <see langword="true"/>, raises <see cref="SettingsChanged"/> after saving.</param>
|
||||||
void Save(bool hotReload = true);
|
void Save(bool hotReload = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atomically applies a transformation to the current settings, persists the result,
|
||||||
|
/// and optionally raises <see cref="SettingsChanged"/>.
|
||||||
|
/// </summary>
|
||||||
|
void UpdateSettings(Func<SettingsModel, SettingsModel> transform, bool hotReload = true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raised after settings are saved with <paramref name="hotReload"/> enabled, or after <see cref="Reload"/>.
|
/// Raised after settings are saved with <paramref name="hotReload"/> enabled, or after <see cref="Reload"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -29,27 +29,38 @@ public sealed class SettingsService : ISettingsService
|
|||||||
_persistence = persistence;
|
_persistence = persistence;
|
||||||
_appInfoService = appInfoService;
|
_appInfoService = appInfoService;
|
||||||
_filePath = SettingsJsonPath();
|
_filePath = SettingsJsonPath();
|
||||||
Settings = _persistence.Load(_filePath, JsonSerializationContext.Default.SettingsModel);
|
_settings = _persistence.Load(_filePath, JsonSerializationContext.Default.SettingsModel);
|
||||||
ApplyMigrations();
|
ApplyMigrations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SettingsModel _settings;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public SettingsModel Settings { get; private set; }
|
public SettingsModel Settings => Volatile.Read(ref _settings);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public event TypedEventHandler<ISettingsService, SettingsModel>? SettingsChanged;
|
public event TypedEventHandler<ISettingsService, SettingsModel>? SettingsChanged;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Save(bool hotReload = true)
|
public void Save(bool hotReload = true) => UpdateSettings(s => s, hotReload);
|
||||||
{
|
|
||||||
_persistence.Save(
|
|
||||||
Settings,
|
|
||||||
_filePath,
|
|
||||||
JsonSerializationContext.Default.SettingsModel);
|
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void UpdateSettings(Func<SettingsModel, SettingsModel> transform, bool hotReload = true)
|
||||||
|
{
|
||||||
|
SettingsModel snapshot;
|
||||||
|
SettingsModel updated;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
snapshot = Volatile.Read(ref _settings);
|
||||||
|
updated = transform(snapshot);
|
||||||
|
}
|
||||||
|
while (Interlocked.CompareExchange(ref _settings, updated, snapshot) != snapshot);
|
||||||
|
|
||||||
|
var newSettings = Volatile.Read(ref _settings);
|
||||||
|
_persistence.Save(newSettings, _filePath, JsonSerializationContext.Default.SettingsModel);
|
||||||
if (hotReload)
|
if (hotReload)
|
||||||
{
|
{
|
||||||
SettingsChanged?.Invoke(this, Settings);
|
SettingsChanged?.Invoke(this, newSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,10 +82,10 @@ public sealed class SettingsService : ISettingsService
|
|||||||
migratedAny |= TryMigrate(
|
migratedAny |= TryMigrate(
|
||||||
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
|
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
|
||||||
root,
|
root,
|
||||||
Settings,
|
ref _settings,
|
||||||
nameof(SettingsModel.AutoGoHomeInterval),
|
nameof(SettingsModel.AutoGoHomeInterval),
|
||||||
DeprecatedHotkeyGoesHomeKey,
|
DeprecatedHotkeyGoesHomeKey,
|
||||||
(model, goesHome) => model.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan,
|
(ref SettingsModel model, bool goesHome) => model = model with { AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan },
|
||||||
JsonSerializationContext.Default.Boolean);
|
JsonSerializationContext.Default.Boolean);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,13 +100,15 @@ public sealed class SettingsService : ISettingsService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private delegate void MigrationApply<T>(ref SettingsModel model, T value);
|
||||||
|
|
||||||
private static bool TryMigrate<T>(
|
private static bool TryMigrate<T>(
|
||||||
string migrationName,
|
string migrationName,
|
||||||
JsonObject root,
|
JsonObject root,
|
||||||
SettingsModel model,
|
ref SettingsModel model,
|
||||||
string newKey,
|
string newKey,
|
||||||
string oldKey,
|
string oldKey,
|
||||||
Action<SettingsModel, T> apply,
|
MigrationApply<T> apply,
|
||||||
JsonTypeInfo<T> jsonTypeInfo)
|
JsonTypeInfo<T> jsonTypeInfo)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -108,7 +121,7 @@ public sealed class SettingsService : ISettingsService
|
|||||||
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
|
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
|
||||||
{
|
{
|
||||||
var value = oldNode.Deserialize(jsonTypeInfo);
|
var value = oldNode.Deserialize(jsonTypeInfo);
|
||||||
apply(model, value!);
|
apply(ref model, value!);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
|
|
||||||
@@ -13,103 +14,93 @@ namespace Microsoft.CmdPal.UI.ViewModels.Settings;
|
|||||||
/// Settings for the Dock. These are settings for _the whole dock_. Band-specific
|
/// Settings for the Dock. These are settings for _the whole dock_. Band-specific
|
||||||
/// settings are in <see cref="DockBandSettings"/>.
|
/// settings are in <see cref="DockBandSettings"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DockSettings
|
public record DockSettings
|
||||||
{
|
{
|
||||||
public DockSide Side { get; set; } = DockSide.Top;
|
public DockSide Side { get; init; } = DockSide.Top;
|
||||||
|
|
||||||
public DockSize DockSize { get; set; } = DockSize.Small;
|
public DockSize DockSize { get; init; } = DockSize.Small;
|
||||||
|
|
||||||
public DockSize DockIconsSize { get; set; } = DockSize.Small;
|
public DockSize DockIconsSize { get; init; } = DockSize.Small;
|
||||||
|
|
||||||
// <Theme settings>
|
// <Theme settings>
|
||||||
public DockBackdrop Backdrop { get; set; } = DockBackdrop.Acrylic;
|
public DockBackdrop Backdrop { get; init; } = DockBackdrop.Acrylic;
|
||||||
|
|
||||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
public UserTheme Theme { get; init; } = UserTheme.Default;
|
||||||
|
|
||||||
public ColorizationMode ColorizationMode { get; set; }
|
public ColorizationMode ColorizationMode { get; init; }
|
||||||
|
|
||||||
public Color CustomThemeColor { get; set; } = new() { A = 0, R = 255, G = 255, B = 255 }; // Transparent — avoids WinUI3 COM dependency on Colors.Transparent and COM in class init
|
public Color CustomThemeColor { get; init; } = new() { A = 0, R = 255, G = 255, B = 255 }; // Transparent — avoids WinUI3 COM dependency on Colors.Transparent and COM in class init
|
||||||
|
|
||||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
public int CustomThemeColorIntensity { get; init; } = 100;
|
||||||
|
|
||||||
public int BackgroundImageOpacity { get; set; } = 20;
|
public int BackgroundImageOpacity { get; init; } = 20;
|
||||||
|
|
||||||
public int BackgroundImageBlurAmount { get; set; }
|
public int BackgroundImageBlurAmount { get; init; }
|
||||||
|
|
||||||
public int BackgroundImageBrightness { get; set; }
|
public int BackgroundImageBrightness { get; init; }
|
||||||
|
|
||||||
public BackgroundImageFit BackgroundImageFit { get; set; }
|
public BackgroundImageFit BackgroundImageFit { get; init; }
|
||||||
|
|
||||||
public string? BackgroundImagePath { get; set; }
|
public string? BackgroundImagePath { get; init; }
|
||||||
|
|
||||||
// </Theme settings>
|
// </Theme settings>
|
||||||
// public List<string> PinnedCommands { get; set; } = [];
|
public ImmutableList<DockBandSettings> StartBands { get; init; } = ImmutableList.Create(
|
||||||
public List<DockBandSettings> StartBands { get; set; } = [];
|
new DockBandSettings
|
||||||
|
|
||||||
public List<DockBandSettings> CenterBands { get; set; } = [];
|
|
||||||
|
|
||||||
public List<DockBandSettings> EndBands { get; set; } = [];
|
|
||||||
|
|
||||||
public bool ShowLabels { get; set; } = true;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public IEnumerable<(string ProviderId, string CommandId)> AllPinnedCommands =>
|
|
||||||
StartBands.Select(b => (b.ProviderId, b.CommandId))
|
|
||||||
.Concat(CenterBands.Select(b => (b.ProviderId, b.CommandId)))
|
|
||||||
.Concat(EndBands.Select(b => (b.ProviderId, b.CommandId)));
|
|
||||||
|
|
||||||
public DockSettings()
|
|
||||||
{
|
|
||||||
// Initialize with default values
|
|
||||||
// PinnedCommands = [
|
|
||||||
// "com.microsoft.cmdpal.winget"
|
|
||||||
// ];
|
|
||||||
StartBands.Add(new DockBandSettings
|
|
||||||
{
|
{
|
||||||
ProviderId = "com.microsoft.cmdpal.builtin.core",
|
ProviderId = "com.microsoft.cmdpal.builtin.core",
|
||||||
CommandId = "com.microsoft.cmdpal.home",
|
CommandId = "com.microsoft.cmdpal.home",
|
||||||
});
|
},
|
||||||
StartBands.Add(new DockBandSettings
|
new DockBandSettings
|
||||||
{
|
{
|
||||||
ProviderId = "WinGet",
|
ProviderId = "WinGet",
|
||||||
CommandId = "com.microsoft.cmdpal.winget",
|
CommandId = "com.microsoft.cmdpal.winget",
|
||||||
ShowLabels = false,
|
ShowLabels = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
EndBands.Add(new DockBandSettings
|
public ImmutableList<DockBandSettings> CenterBands { get; init; } = ImmutableList<DockBandSettings>.Empty;
|
||||||
|
|
||||||
|
public ImmutableList<DockBandSettings> EndBands { get; init; } = ImmutableList.Create(
|
||||||
|
new DockBandSettings
|
||||||
{
|
{
|
||||||
ProviderId = "PerformanceMonitor",
|
ProviderId = "PerformanceMonitor",
|
||||||
CommandId = "com.microsoft.cmdpal.performanceWidget",
|
CommandId = "com.microsoft.cmdpal.performanceWidget",
|
||||||
});
|
},
|
||||||
EndBands.Add(new DockBandSettings
|
new DockBandSettings
|
||||||
{
|
{
|
||||||
ProviderId = "com.microsoft.cmdpal.builtin.datetime",
|
ProviderId = "com.microsoft.cmdpal.builtin.datetime",
|
||||||
CommandId = "com.microsoft.cmdpal.timedate.dockBand",
|
CommandId = "com.microsoft.cmdpal.timedate.dockBand",
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
public bool ShowLabels { get; init; } = true;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public IEnumerable<(string ProviderId, string CommandId)> AllPinnedCommands =>
|
||||||
|
StartBands.Select(b => (b.ProviderId, b.CommandId))
|
||||||
|
.Concat(CenterBands.Select(b => (b.ProviderId, b.CommandId)))
|
||||||
|
.Concat(EndBands.Select(b => (b.ProviderId, b.CommandId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Settings for a specific dock band. These are per-band settings stored
|
/// Settings for a specific dock band. These are per-band settings stored
|
||||||
/// within the overall <see cref="DockSettings"/>.
|
/// within the overall <see cref="DockSettings"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DockBandSettings
|
public record DockBandSettings
|
||||||
{
|
{
|
||||||
public required string ProviderId { get; set; }
|
public required string ProviderId { get; init; }
|
||||||
|
|
||||||
public required string CommandId { get; set; }
|
public required string CommandId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether titles are shown for items in this band.
|
/// Gets or sets whether titles are shown for items in this band.
|
||||||
/// If null, falls back to dock-wide ShowLabels setting.
|
/// If null, falls back to dock-wide ShowLabels setting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? ShowTitles { get; set; }
|
public bool? ShowTitles { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether subtitles are shown for items in this band.
|
/// Gets or sets whether subtitles are shown for items in this band.
|
||||||
/// If null, falls back to dock-wide ShowLabels setting.
|
/// If null, falls back to dock-wide ShowLabels setting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? ShowSubtitles { get; set; }
|
public bool? ShowSubtitles { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value for backward compatibility. Maps to ShowTitles.
|
/// Gets or sets a value for backward compatibility. Maps to ShowTitles.
|
||||||
@@ -118,7 +109,7 @@ public class DockBandSettings
|
|||||||
public bool? ShowLabels
|
public bool? ShowLabels
|
||||||
{
|
{
|
||||||
get => ShowTitles;
|
get => ShowTitles;
|
||||||
set => ShowTitles = value;
|
init => ShowTitles = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -134,17 +125,6 @@ public class DockBandSettings
|
|||||||
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
|
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ResolveShowSubtitles(bool defaultValue) => ShowSubtitles ?? defaultValue;
|
public bool ResolveShowSubtitles(bool defaultValue) => ShowSubtitles ?? defaultValue;
|
||||||
|
|
||||||
public DockBandSettings Clone()
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
ProviderId = this.ProviderId,
|
|
||||||
CommandId = this.CommandId,
|
|
||||||
ShowTitles = this.ShowTitles,
|
|
||||||
ShowSubtitles = this.ShowSubtitles,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum DockSide
|
public enum DockSide
|
||||||
|
|||||||
@@ -39,24 +39,24 @@ public record HotkeySettings// : ICmdLineRepresentable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("win")]
|
[JsonPropertyName("win")]
|
||||||
public bool Win { get; set; }
|
public bool Win { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("ctrl")]
|
[JsonPropertyName("ctrl")]
|
||||||
public bool Ctrl { get; set; }
|
public bool Ctrl { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("alt")]
|
[JsonPropertyName("alt")]
|
||||||
public bool Alt { get; set; }
|
public bool Alt { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("shift")]
|
[JsonPropertyName("shift")]
|
||||||
public bool Shift { get; set; }
|
public bool Shift { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("code")]
|
[JsonPropertyName("code")]
|
||||||
public int Code { get; set; }
|
public int Code { get; init; }
|
||||||
|
|
||||||
// This is currently needed for FancyZones, we need to unify these two objects
|
// This is currently needed for FancyZones, we need to unify these two objects
|
||||||
// see src\common\settings_objects.h
|
// see src\common\settings_objects.h
|
||||||
[JsonPropertyName("key")]
|
[JsonPropertyName("key")]
|
||||||
public string Key { get; set; } = string.Empty;
|
public string Key { get; init; } = string.Empty;
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,106 +2,114 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
public partial class SettingsModel : ObservableObject
|
public record SettingsModel
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// SETTINGS HERE
|
// SETTINGS HERE
|
||||||
public static HotkeySettings DefaultActivationShortcut { get; } = new HotkeySettings(true, false, true, false, 0x20); // win+alt+space
|
public static HotkeySettings DefaultActivationShortcut { get; } = new HotkeySettings(true, false, true, false, 0x20); // win+alt+space
|
||||||
|
|
||||||
public HotkeySettings? Hotkey { get; set; } = DefaultActivationShortcut;
|
public HotkeySettings? Hotkey { get; init; } = DefaultActivationShortcut;
|
||||||
|
|
||||||
public bool UseLowLevelGlobalHotkey { get; set; }
|
public bool UseLowLevelGlobalHotkey { get; init; }
|
||||||
|
|
||||||
public bool ShowAppDetails { get; set; }
|
public bool ShowAppDetails { get; init; }
|
||||||
|
|
||||||
public bool BackspaceGoesBack { get; set; }
|
public bool BackspaceGoesBack { get; init; }
|
||||||
|
|
||||||
public bool SingleClickActivates { get; set; }
|
public bool SingleClickActivates { get; init; }
|
||||||
|
|
||||||
public bool HighlightSearchOnActivate { get; set; } = true;
|
public bool HighlightSearchOnActivate { get; init; } = true;
|
||||||
|
|
||||||
public bool KeepPreviousQuery { get; set; }
|
public bool KeepPreviousQuery { get; init; }
|
||||||
|
|
||||||
public bool ShowSystemTrayIcon { get; set; } = true;
|
public bool ShowSystemTrayIcon { get; init; } = true;
|
||||||
|
|
||||||
public bool IgnoreShortcutWhenFullscreen { get; set; }
|
public bool IgnoreShortcutWhenFullscreen { get; init; }
|
||||||
|
|
||||||
public bool AllowExternalReload { get; set; }
|
public bool AllowExternalReload { get; init; }
|
||||||
|
|
||||||
public Dictionary<string, ProviderSettings> ProviderSettings { get; set; } = [];
|
public ImmutableDictionary<string, ProviderSettings> ProviderSettings { get; init; }
|
||||||
|
= ImmutableDictionary<string, ProviderSettings>.Empty;
|
||||||
|
|
||||||
public string[] FallbackRanks { get; set; } = [];
|
public string[] FallbackRanks { get; init; } = [];
|
||||||
|
|
||||||
public Dictionary<string, CommandAlias> Aliases { get; set; } = [];
|
public ImmutableDictionary<string, CommandAlias> Aliases { get; init; }
|
||||||
|
= ImmutableDictionary<string, CommandAlias>.Empty;
|
||||||
|
|
||||||
public List<TopLevelHotkey> CommandHotkeys { get; set; } = [];
|
public ImmutableList<TopLevelHotkey> CommandHotkeys { get; init; }
|
||||||
|
= ImmutableList<TopLevelHotkey>.Empty;
|
||||||
|
|
||||||
public MonitorBehavior SummonOn { get; set; } = MonitorBehavior.ToMouse;
|
public MonitorBehavior SummonOn { get; init; } = MonitorBehavior.ToMouse;
|
||||||
|
|
||||||
public bool DisableAnimations { get; set; } = true;
|
public bool DisableAnimations { get; init; } = true;
|
||||||
|
|
||||||
public WindowPosition? LastWindowPosition { get; set; }
|
public WindowPosition? LastWindowPosition { get; init; }
|
||||||
|
|
||||||
public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan;
|
public TimeSpan AutoGoHomeInterval { get; init; } = Timeout.InfiniteTimeSpan;
|
||||||
|
|
||||||
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; init; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
||||||
|
|
||||||
public bool EnableDock { get; set; }
|
public bool EnableDock { get; init; }
|
||||||
|
|
||||||
public DockSettings DockSettings { get; set; } = new();
|
public DockSettings DockSettings { get; init; } = new();
|
||||||
|
|
||||||
// Theme settings
|
// Theme settings
|
||||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
public UserTheme Theme { get; init; } = UserTheme.Default;
|
||||||
|
|
||||||
public ColorizationMode ColorizationMode { get; set; }
|
public ColorizationMode ColorizationMode { get; init; }
|
||||||
|
|
||||||
public Color CustomThemeColor { get; set; } = new() { A = 0, R = 255, G = 255, B = 255 }; // Transparent — avoids WinUI3 COM dependency on Colors.Transparent
|
public Color CustomThemeColor { get; init; } = new() { A = 0, R = 255, G = 255, B = 255 }; // Transparent — avoids WinUI3 COM dependency on Colors.Transparent
|
||||||
|
|
||||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
public int CustomThemeColorIntensity { get; init; } = 100;
|
||||||
|
|
||||||
public int BackgroundImageTintIntensity { get; set; }
|
public int BackgroundImageTintIntensity { get; init; }
|
||||||
|
|
||||||
public int BackgroundImageOpacity { get; set; } = 20;
|
public int BackgroundImageOpacity { get; init; } = 20;
|
||||||
|
|
||||||
public int BackgroundImageBlurAmount { get; set; }
|
public int BackgroundImageBlurAmount { get; init; }
|
||||||
|
|
||||||
public int BackgroundImageBrightness { get; set; }
|
public int BackgroundImageBrightness { get; init; }
|
||||||
|
|
||||||
public BackgroundImageFit BackgroundImageFit { get; set; }
|
public BackgroundImageFit BackgroundImageFit { get; init; }
|
||||||
|
|
||||||
public string? BackgroundImagePath { get; set; }
|
public string? BackgroundImagePath { get; init; }
|
||||||
|
|
||||||
public BackdropStyle BackdropStyle { get; set; }
|
public BackdropStyle BackdropStyle { get; init; }
|
||||||
|
|
||||||
public int BackdropOpacity { get; set; } = 100;
|
public int BackdropOpacity { get; init; } = 100;
|
||||||
|
|
||||||
// </Theme settings>
|
// </Theme settings>
|
||||||
|
|
||||||
// END SETTINGS
|
// END SETTINGS
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public ProviderSettings GetProviderSettings(CommandProviderWrapper provider)
|
public (SettingsModel Model, ProviderSettings Settings) GetProviderSettings(CommandProviderWrapper provider)
|
||||||
{
|
{
|
||||||
ProviderSettings? settings;
|
if (!ProviderSettings.TryGetValue(provider.ProviderId, out var settings))
|
||||||
if (!ProviderSettings.TryGetValue(provider.ProviderId, out settings))
|
|
||||||
{
|
{
|
||||||
settings = new ProviderSettings(provider);
|
settings = new ProviderSettings();
|
||||||
settings.Connect(provider);
|
|
||||||
ProviderSettings[provider.ProviderId] = settings;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
settings.Connect(provider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings;
|
var connected = settings.WithConnection(provider);
|
||||||
|
|
||||||
|
// If WithConnection returned the same instance, nothing changed — skip SetItem
|
||||||
|
if (ReferenceEquals(connected, settings))
|
||||||
|
{
|
||||||
|
return (this, connected);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newModel = this with
|
||||||
|
{
|
||||||
|
ProviderSettings = ProviderSettings.SetItem(provider.ProviderId, connected),
|
||||||
|
};
|
||||||
|
return (newModel, connected);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] GetGlobalFallbacks()
|
public string[] GetGlobalFallbacks()
|
||||||
@@ -150,6 +158,13 @@ public partial class SettingsModel : ObservableObject
|
|||||||
[JsonSerializable(typeof(RecentCommandsManager))]
|
[JsonSerializable(typeof(RecentCommandsManager))]
|
||||||
[JsonSerializable(typeof(List<string>), TypeInfoPropertyName = "StringList")]
|
[JsonSerializable(typeof(List<string>), TypeInfoPropertyName = "StringList")]
|
||||||
[JsonSerializable(typeof(List<HistoryItem>), TypeInfoPropertyName = "HistoryList")]
|
[JsonSerializable(typeof(List<HistoryItem>), TypeInfoPropertyName = "HistoryList")]
|
||||||
|
[JsonSerializable(typeof(ImmutableList<HistoryItem>), TypeInfoPropertyName = "ImmutableHistoryList")]
|
||||||
|
[JsonSerializable(typeof(ImmutableDictionary<string, FallbackSettings>), TypeInfoPropertyName = "ImmutableFallbackDictionary")]
|
||||||
|
[JsonSerializable(typeof(ImmutableList<string>), TypeInfoPropertyName = "ImmutableStringList")]
|
||||||
|
[JsonSerializable(typeof(ImmutableList<DockBandSettings>), TypeInfoPropertyName = "ImmutableDockBandSettingsList")]
|
||||||
|
[JsonSerializable(typeof(ImmutableDictionary<string, ProviderSettings>), TypeInfoPropertyName = "ImmutableProviderSettingsDictionary")]
|
||||||
|
[JsonSerializable(typeof(ImmutableDictionary<string, CommandAlias>), TypeInfoPropertyName = "ImmutableAliasDictionary")]
|
||||||
|
[JsonSerializable(typeof(ImmutableList<TopLevelHotkey>), TypeInfoPropertyName = "ImmutableTopLevelHotkeyList")]
|
||||||
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
|
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
|
||||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
|
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
|
||||||
|
|||||||
@@ -41,9 +41,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.Hotkey;
|
get => _settingsService.Settings.Hotkey;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.Hotkey = value ?? SettingsModel.DefaultActivationShortcut;
|
_settingsService.UpdateSettings(s => s with { Hotkey = value ?? SettingsModel.DefaultActivationShortcut });
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,9 +51,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.UseLowLevelGlobalHotkey;
|
get => _settingsService.Settings.UseLowLevelGlobalHotkey;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.UseLowLevelGlobalHotkey = value;
|
_settingsService.UpdateSettings(s => s with { UseLowLevelGlobalHotkey = value });
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +61,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.AllowExternalReload;
|
get => _settingsService.Settings.AllowExternalReload;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.AllowExternalReload = value;
|
_settingsService.UpdateSettings(s => s with { AllowExternalReload = value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +70,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.ShowAppDetails;
|
get => _settingsService.Settings.ShowAppDetails;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.ShowAppDetails = value;
|
_settingsService.UpdateSettings(s => s with { ShowAppDetails = value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +79,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.BackspaceGoesBack;
|
get => _settingsService.Settings.BackspaceGoesBack;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.BackspaceGoesBack = value;
|
_settingsService.UpdateSettings(s => s with { BackspaceGoesBack = value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,8 +88,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.SingleClickActivates;
|
get => _settingsService.Settings.SingleClickActivates;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.SingleClickActivates = value;
|
_settingsService.UpdateSettings(s => s with { SingleClickActivates = value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,8 +97,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.HighlightSearchOnActivate;
|
get => _settingsService.Settings.HighlightSearchOnActivate;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.HighlightSearchOnActivate = value;
|
_settingsService.UpdateSettings(s => s with { HighlightSearchOnActivate = value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,8 +106,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.KeepPreviousQuery;
|
get => _settingsService.Settings.KeepPreviousQuery;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.KeepPreviousQuery = value;
|
_settingsService.UpdateSettings(s => s with { KeepPreviousQuery = value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,8 +115,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => (int)_settingsService.Settings.SummonOn;
|
get => (int)_settingsService.Settings.SummonOn;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.SummonOn = (MonitorBehavior)value;
|
_settingsService.UpdateSettings(s => s with { SummonOn = (MonitorBehavior)value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,8 +124,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.ShowSystemTrayIcon;
|
get => _settingsService.Settings.ShowSystemTrayIcon;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.ShowSystemTrayIcon = value;
|
_settingsService.UpdateSettings(s => s with { ShowSystemTrayIcon = value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,8 +133,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.IgnoreShortcutWhenFullscreen;
|
get => _settingsService.Settings.IgnoreShortcutWhenFullscreen;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.IgnoreShortcutWhenFullscreen = value;
|
_settingsService.UpdateSettings(s => s with { IgnoreShortcutWhenFullscreen = value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,8 +142,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.DisableAnimations;
|
get => _settingsService.Settings.DisableAnimations;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DisableAnimations = value;
|
_settingsService.UpdateSettings(s => s with { DisableAnimations = value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,10 +158,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
{
|
{
|
||||||
if (value >= 0 && value < AutoGoHomeIntervals.Count)
|
if (value >= 0 && value < AutoGoHomeIntervals.Count)
|
||||||
{
|
{
|
||||||
_settingsService.Settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
|
_settingsService.UpdateSettings(s => s with { AutoGoHomeInterval = AutoGoHomeIntervals[value] });
|
||||||
}
|
}
|
||||||
|
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,8 +168,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => (int)_settingsService.Settings.EscapeKeyBehaviorSetting;
|
get => (int)_settingsService.Settings.EscapeKeyBehaviorSetting;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.EscapeKeyBehaviorSetting = (EscapeKeyBehavior)value;
|
_settingsService.UpdateSettings(s => s with { EscapeKeyBehaviorSetting = (EscapeKeyBehavior)value });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,8 +177,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.DockSettings.Side;
|
get => _settingsService.Settings.DockSettings.Side;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.Side = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { Side = value } });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,8 +186,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.DockSettings.DockSize;
|
get => _settingsService.Settings.DockSettings.DockSize;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.DockSize = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { DockSize = value } });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +195,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.DockSettings.Backdrop;
|
get => _settingsService.Settings.DockSettings.Backdrop;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.Backdrop = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { Backdrop = value } });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,8 +204,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.DockSettings.ShowLabels;
|
get => _settingsService.Settings.DockSettings.ShowLabels;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.DockSettings.ShowLabels = value;
|
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { ShowLabels = value } });
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,8 +213,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
get => _settingsService.Settings.EnableDock;
|
get => _settingsService.Settings.EnableDock;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_settingsService.Settings.EnableDock = value;
|
_settingsService.UpdateSettings(s => s with { EnableDock = value });
|
||||||
Save();
|
|
||||||
WeakReferenceMessenger.Default.Send(new ShowHideDockMessage(value));
|
WeakReferenceMessenger.Default.Send(new ShowHideDockMessage(value));
|
||||||
WeakReferenceMessenger.Default.Send(new ReloadCommandsMessage()); // TODO! we need to update the MoreCommands of all top level items, but we don't _really_ want to reload
|
WeakReferenceMessenger.Default.Send(new ReloadCommandsMessage()); // TODO! we need to update the MoreCommands of all top level items, but we don't _really_ want to reload
|
||||||
}
|
}
|
||||||
@@ -245,7 +225,11 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
|
|
||||||
public SettingsExtensionsViewModel Extensions { get; }
|
public SettingsExtensionsViewModel Extensions { get; }
|
||||||
|
|
||||||
public SettingsViewModel(TopLevelCommandManager topLevelCommandManager, TaskScheduler scheduler, IThemeService themeService, ISettingsService settingsService)
|
public SettingsViewModel(
|
||||||
|
TopLevelCommandManager topLevelCommandManager,
|
||||||
|
TaskScheduler scheduler,
|
||||||
|
IThemeService themeService,
|
||||||
|
ISettingsService settingsService)
|
||||||
{
|
{
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_topLevelCommandManager = topLevelCommandManager;
|
_topLevelCommandManager = topLevelCommandManager;
|
||||||
@@ -259,15 +243,27 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
var fallbacks = new List<FallbackSettingsViewModel>();
|
var fallbacks = new List<FallbackSettingsViewModel>();
|
||||||
var currentRankings = _settingsService.Settings.FallbackRanks;
|
var currentRankings = _settingsService.Settings.FallbackRanks;
|
||||||
var needsSave = false;
|
var needsSave = false;
|
||||||
|
var currentSettingsModel = _settingsService.Settings;
|
||||||
|
|
||||||
foreach (var item in activeProviders)
|
foreach (var item in activeProviders)
|
||||||
{
|
{
|
||||||
var providerSettings = _settingsService.Settings.GetProviderSettings(item);
|
var (newModel, providerSettings) = currentSettingsModel.GetProviderSettings(item);
|
||||||
|
currentSettingsModel = newModel;
|
||||||
|
|
||||||
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, settingsService);
|
var providerSettingsModel = new ProviderSettingsViewModel(item, providerSettings, settingsService);
|
||||||
CommandProviders.Add(settingsModel);
|
CommandProviders.Add(providerSettingsModel);
|
||||||
|
|
||||||
fallbacks.AddRange(settingsModel.FallbackCommands);
|
fallbacks.AddRange(providerSettingsModel.FallbackCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only persist if provider enumeration actually changed the model
|
||||||
|
// Smelly? Yes, but it avoids an unnecessary write to disk.
|
||||||
|
// I don't love it, but it seems better than the alternatives.
|
||||||
|
// Open to suggestions.
|
||||||
|
if (!ReferenceEquals(currentSettingsModel, _settingsService.Settings))
|
||||||
|
{
|
||||||
|
var finalModel = currentSettingsModel;
|
||||||
|
_settingsService.UpdateSettings(_ => finalModel, hotReload: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fallbackRankings = new List<Scored<FallbackSettingsViewModel>>(fallbacks.Count);
|
var fallbackRankings = new List<Scored<FallbackSettingsViewModel>>(fallbacks.Count);
|
||||||
@@ -306,10 +302,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
|||||||
|
|
||||||
public void ApplyFallbackSort()
|
public void ApplyFallbackSort()
|
||||||
{
|
{
|
||||||
_settingsService.Settings.FallbackRanks = FallbackRankings.Select(s => s.Id).ToArray();
|
_settingsService.UpdateSettings(s => s with { FallbackRanks = FallbackRankings.Select(s2 => s2.Id).ToArray() });
|
||||||
Save();
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FallbackRankings)));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FallbackRankings)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Save() => _settingsService.Save();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ using Microsoft.CmdPal.UI.ViewModels.Settings;
|
|||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
public class TopLevelHotkey(HotkeySettings? hotkey, string commandId)
|
public record TopLevelHotkey
|
||||||
{
|
{
|
||||||
public string CommandId { get; set; } = commandId;
|
public string CommandId { get; init; }
|
||||||
|
|
||||||
public HotkeySettings? Hotkey { get; set; } = hotkey;
|
public HotkeySettings? Hotkey { get; init; }
|
||||||
|
|
||||||
|
public TopLevelHotkey(HotkeySettings? hotkey, string commandId)
|
||||||
|
{
|
||||||
|
Hotkey = hotkey;
|
||||||
|
CommandId = commandId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
|||||||
{
|
{
|
||||||
if (Alias is CommandAlias a)
|
if (Alias is CommandAlias a)
|
||||||
{
|
{
|
||||||
a.Alias = value;
|
Alias = a with { Alias = value };
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -146,7 +146,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
|||||||
{
|
{
|
||||||
if (Alias is CommandAlias a)
|
if (Alias is CommandAlias a)
|
||||||
{
|
{
|
||||||
a.IsDirect = value;
|
Alias = a with { IsDirect = value };
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleChangeAlias();
|
HandleChangeAlias();
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
|
|||||||
var providerId = providerContext.ProviderId;
|
var providerId = providerContext.ProviderId;
|
||||||
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
|
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
|
||||||
{
|
{
|
||||||
var providerSettings = _settingsService.Settings.GetProviderSettings(provider);
|
var (_, providerSettings) = _settingsService.Settings.GetProviderSettings(provider);
|
||||||
|
|
||||||
var alreadyPinnedToTopLevel = providerSettings.PinnedCommandIds.Contains(itemId);
|
var alreadyPinnedToTopLevel = providerSettings.PinnedCommandIds.Contains(itemId);
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
|
|||||||
var providerId = providerContext.ProviderId;
|
var providerId = providerContext.ProviderId;
|
||||||
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
|
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
|
||||||
{
|
{
|
||||||
var providerSettings = _settingsService.Settings.GetProviderSettings(provider);
|
var (_, providerSettings) = _settingsService.Settings.GetProviderSettings(provider);
|
||||||
|
|
||||||
var isPinnedSubCommand = providerSettings.PinnedCommandIds.Contains(itemId);
|
var isPinnedSubCommand = providerSettings.PinnedCommandIds.Contains(itemId);
|
||||||
if (isPinnedSubCommand)
|
if (isPinnedSubCommand)
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
|||||||
_ = _modifierKeysOnEntering.Remove(virtualKey);
|
_ = _modifierKeysOnEntering.Remove(virtualKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
internalSettings.Win = matchValue;
|
internalSettings = internalSettings with { Win = matchValue };
|
||||||
break;
|
break;
|
||||||
case VirtualKey.Control:
|
case VirtualKey.Control:
|
||||||
case VirtualKey.LeftControl:
|
case VirtualKey.LeftControl:
|
||||||
@@ -197,7 +197,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
|||||||
_ = _modifierKeysOnEntering.Remove(VirtualKey.Control);
|
_ = _modifierKeysOnEntering.Remove(VirtualKey.Control);
|
||||||
}
|
}
|
||||||
|
|
||||||
internalSettings.Ctrl = matchValue;
|
internalSettings = internalSettings with { Ctrl = matchValue };
|
||||||
break;
|
break;
|
||||||
case VirtualKey.Menu:
|
case VirtualKey.Menu:
|
||||||
case VirtualKey.LeftMenu:
|
case VirtualKey.LeftMenu:
|
||||||
@@ -208,7 +208,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
|||||||
_ = _modifierKeysOnEntering.Remove(VirtualKey.Menu);
|
_ = _modifierKeysOnEntering.Remove(VirtualKey.Menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
internalSettings.Alt = matchValue;
|
internalSettings = internalSettings with { Alt = matchValue };
|
||||||
break;
|
break;
|
||||||
case VirtualKey.Shift:
|
case VirtualKey.Shift:
|
||||||
case VirtualKey.LeftShift:
|
case VirtualKey.LeftShift:
|
||||||
@@ -219,14 +219,14 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
|||||||
_ = _modifierKeysOnEntering.Remove(VirtualKey.Shift);
|
_ = _modifierKeysOnEntering.Remove(VirtualKey.Shift);
|
||||||
}
|
}
|
||||||
|
|
||||||
internalSettings.Shift = matchValue;
|
internalSettings = internalSettings with { Shift = matchValue };
|
||||||
break;
|
break;
|
||||||
case VirtualKey.Escape:
|
case VirtualKey.Escape:
|
||||||
internalSettings = new HotkeySettings();
|
internalSettings = new HotkeySettings();
|
||||||
shortcutDialog.IsPrimaryButtonEnabled = false;
|
shortcutDialog.IsPrimaryButtonEnabled = false;
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
internalSettings.Code = matchValueCode;
|
internalSettings = internalSettings with { Code = matchValueCode };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
|||||||
else if (internalSettings.Shift && !_modifierKeysOnEntering.Contains(VirtualKey.Shift) && !internalSettings.Win && !internalSettings.Alt && !internalSettings.Ctrl)
|
else if (internalSettings.Shift && !_modifierKeysOnEntering.Contains(VirtualKey.Shift) && !internalSettings.Win && !internalSettings.Alt && !internalSettings.Ctrl)
|
||||||
{
|
{
|
||||||
// This is to reset the shift key press within the control as it was not used within the control but rather was used to leave the hotkey.
|
// This is to reset the shift key press within the control as it was not used within the control but rather was used to leave the hotkey.
|
||||||
internalSettings.Shift = false;
|
internalSettings = internalSettings with { Shift = false };
|
||||||
|
|
||||||
SendSingleKeyboardInput((short)VirtualKey.Shift, (uint)NativeKeyboardHelper.KeyEventF.KeyDown);
|
SendSingleKeyboardInput((short)VirtualKey.Shift, (uint)NativeKeyboardHelper.KeyEventF.KeyDown);
|
||||||
|
|
||||||
|
|||||||
@@ -910,8 +910,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
// the last non-dock placement because dock sessions intentionally skip updates.
|
// the last non-dock placement because dock sessions intentionally skip updates.
|
||||||
if (_currentWindowPosition.IsSizeValid)
|
if (_currentWindowPosition.IsSizeValid)
|
||||||
{
|
{
|
||||||
settings.LastWindowPosition = _currentWindowPosition;
|
settingsService.UpdateSettings(s => s with { LastWindowPosition = _currentWindowPosition });
|
||||||
settingsService.Save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Immutable;
|
||||||
using Microsoft.CmdPal.Common.Services;
|
using Microsoft.CmdPal.Common.Services;
|
||||||
using Microsoft.CmdPal.UI.ViewModels;
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||||
@@ -19,10 +20,13 @@ internal sealed class RunHistoryService : IRunHistoryService
|
|||||||
|
|
||||||
public IReadOnlyList<string> GetRunHistory()
|
public IReadOnlyList<string> GetRunHistory()
|
||||||
{
|
{
|
||||||
if (_appStateService.State.RunHistory.Count == 0)
|
if (_appStateService.State.RunHistory.IsEmpty)
|
||||||
{
|
{
|
||||||
var history = Microsoft.Terminal.UI.RunHistory.CreateRunHistory();
|
var history = Microsoft.Terminal.UI.RunHistory.CreateRunHistory();
|
||||||
_appStateService.State.RunHistory.AddRange(history);
|
_appStateService.UpdateState(state => state with
|
||||||
|
{
|
||||||
|
RunHistory = history.ToImmutableList(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return _appStateService.State.RunHistory;
|
return _appStateService.State.RunHistory;
|
||||||
@@ -30,22 +34,24 @@ internal sealed class RunHistoryService : IRunHistoryService
|
|||||||
|
|
||||||
public void ClearRunHistory()
|
public void ClearRunHistory()
|
||||||
{
|
{
|
||||||
_appStateService.State.RunHistory.Clear();
|
_appStateService.UpdateState(state => state with
|
||||||
|
{
|
||||||
|
RunHistory = ImmutableList<string>.Empty,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddRunHistoryItem(string item)
|
public void AddRunHistoryItem(string item)
|
||||||
{
|
{
|
||||||
// insert at the beginning of the list
|
|
||||||
if (string.IsNullOrWhiteSpace(item))
|
if (string.IsNullOrWhiteSpace(item))
|
||||||
{
|
{
|
||||||
return; // Do not add empty or whitespace items
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_appStateService.State.RunHistory.Remove(item);
|
_appStateService.UpdateState(state => state with
|
||||||
|
{
|
||||||
// Add the item to the front of the history
|
RunHistory = state.RunHistory
|
||||||
_appStateService.State.RunHistory.Insert(0, item);
|
.Remove(item)
|
||||||
|
.Insert(0, item),
|
||||||
_appStateService.Save();
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Microsoft.CmdPal.Common.Services;
|
using Microsoft.CmdPal.Common.Services;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||||
@@ -41,7 +42,7 @@ public class AppStateServiceTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
var expectedState = new AppStateModel
|
var expectedState = new AppStateModel
|
||||||
{
|
{
|
||||||
RunHistory = new List<string> { "command1", "command2" },
|
RunHistory = ImmutableList.Create("command1", "command2"),
|
||||||
};
|
};
|
||||||
_mockPersistence
|
_mockPersistence
|
||||||
.Setup(p => p.Load(
|
.Setup(p => p.Load(
|
||||||
@@ -86,7 +87,8 @@ public class AppStateServiceTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object);
|
var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object);
|
||||||
service.State.RunHistory.Add("test-command");
|
service.UpdateState(s => s with { RunHistory = s.RunHistory.Add("test-command") });
|
||||||
|
_mockPersistence.Invocations.Clear(); // Reset after Arrange — UpdateState also persists
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
service.Save();
|
service.Save();
|
||||||
@@ -160,4 +162,44 @@ public class AppStateServiceTests
|
|||||||
// Assert
|
// Assert
|
||||||
Assert.AreEqual(2, eventCount);
|
Assert.AreEqual(2, eventCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void UpdateState_ConcurrentUpdates_NoLostUpdates()
|
||||||
|
{
|
||||||
|
// Arrange — two threads each add items to RunHistory concurrently.
|
||||||
|
// With the CAS loop, every add must land (no lost updates).
|
||||||
|
var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object);
|
||||||
|
const int iterations = 50;
|
||||||
|
var barrier = new System.Threading.Barrier(2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var t1 = System.Threading.Tasks.Task.Run(() =>
|
||||||
|
{
|
||||||
|
barrier.SignalAndWait();
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
|
{
|
||||||
|
service.UpdateState(s => s with { RunHistory = s.RunHistory.Add($"t1-{i}") });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var t2 = System.Threading.Tasks.Task.Run(() =>
|
||||||
|
{
|
||||||
|
barrier.SignalAndWait();
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
|
{
|
||||||
|
service.UpdateState(s => s with { RunHistory = s.RunHistory.Add($"t2-{i}") });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
System.Threading.Tasks.Task.WaitAll(t1, t2);
|
||||||
|
|
||||||
|
// Assert — all 100 items must be present (no lost updates)
|
||||||
|
Assert.AreEqual(iterations * 2, service.State.RunHistory.Count, "All concurrent updates should be preserved");
|
||||||
|
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(service.State.RunHistory.Contains($"t1-{i}"), $"Missing t1-{i}");
|
||||||
|
Assert.IsTrue(service.State.RunHistory.Contains($"t2-{i}"), $"Missing t2-{i}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
{
|
{
|
||||||
foreach (var item in commandIds)
|
foreach (var item in commandIds)
|
||||||
{
|
{
|
||||||
history.AddHistoryItem(item);
|
history = history.WithHistoryItem(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
var history = CreateHistory();
|
var history = CreateHistory();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
history.AddHistoryItem("com.microsoft.cmdpal.shell");
|
history = history.WithHistoryItem("com.microsoft.cmdpal.shell");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsTrue(history.GetCommandHistoryWeight("com.microsoft.cmdpal.shell") > 0);
|
Assert.IsTrue(history.GetCommandHistoryWeight("com.microsoft.cmdpal.shell") > 0);
|
||||||
@@ -121,7 +121,7 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
var history = new RecentCommandsManager();
|
var history = new RecentCommandsManager();
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
history.AddHistoryItem(item.Id);
|
history = history.WithHistoryItem(item.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return history;
|
return history;
|
||||||
@@ -417,7 +417,7 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
// Add extra uses of VS Code to try and push it above Terminal
|
// Add extra uses of VS Code to try and push it above Terminal
|
||||||
for (var i = 0; i < 10; i++)
|
for (var i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
history.AddHistoryItem(items[1].Id);
|
history = history.WithHistoryItem(items[1].Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
|
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
|
||||||
@@ -446,7 +446,7 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
var vsCodeId = items[1].Id;
|
var vsCodeId = items[1].Id;
|
||||||
for (var i = 0; i < 10; i++)
|
for (var i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
history.AddHistoryItem(vsCodeId);
|
history = history.WithHistoryItem(vsCodeId);
|
||||||
|
|
||||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
|
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
|
||||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||||
|
|||||||
@@ -75,7 +75,14 @@ public class SettingsServiceTests
|
|||||||
public void Settings_ReturnsLoadedModel()
|
public void Settings_ReturnsLoadedModel()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_testSettings.ShowAppDetails = true;
|
_testSettings = _testSettings with { ShowAppDetails = true };
|
||||||
|
|
||||||
|
// Reset mock to return updated settings
|
||||||
|
_mockPersistence
|
||||||
|
.Setup(p => p.Load(
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<SettingsModel>>()))
|
||||||
|
.Returns(_testSettings);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
|
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
|
||||||
@@ -89,7 +96,10 @@ public class SettingsServiceTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
|
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
|
||||||
service.Settings.SingleClickActivates = true;
|
service.UpdateSettings(
|
||||||
|
s => s with { SingleClickActivates = true },
|
||||||
|
hotReload: false);
|
||||||
|
_mockPersistence.Invocations.Clear(); // Reset after Arrange — UpdateSettings also persists
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
service.Save(hotReload: false);
|
service.Save(hotReload: false);
|
||||||
@@ -178,4 +188,40 @@ public class SettingsServiceTests
|
|||||||
Assert.AreSame(service, receivedSender);
|
Assert.AreSame(service, receivedSender);
|
||||||
Assert.AreSame(service.Settings, receivedSettings);
|
Assert.AreSame(service.Settings, receivedSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void UpdateSettings_ConcurrentUpdates_NoLostUpdates()
|
||||||
|
{
|
||||||
|
// Arrange — two threads each set a different property to true, 100 times.
|
||||||
|
// Without a CAS loop, one thread's Exchange can overwrite the other's
|
||||||
|
// property back to false from a stale snapshot. With CAS, both survive.
|
||||||
|
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
|
||||||
|
const int iterations = 100;
|
||||||
|
var barrier = new System.Threading.Barrier(2);
|
||||||
|
|
||||||
|
// Act — t1 sets ShowAppDetails=true, t2 sets SingleClickActivates=true
|
||||||
|
var t1 = System.Threading.Tasks.Task.Run(() =>
|
||||||
|
{
|
||||||
|
barrier.SignalAndWait();
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
|
{
|
||||||
|
service.UpdateSettings(s => s with { ShowAppDetails = true }, hotReload: false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var t2 = System.Threading.Tasks.Task.Run(() =>
|
||||||
|
{
|
||||||
|
barrier.SignalAndWait();
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
|
{
|
||||||
|
service.UpdateSettings(s => s with { SingleClickActivates = true }, hotReload: false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
System.Threading.Tasks.Task.WaitAll(t1, t2);
|
||||||
|
|
||||||
|
// Assert — both properties must be true; neither should have been overwritten
|
||||||
|
Assert.IsTrue(service.Settings.ShowAppDetails, "ShowAppDetails lost — a stale snapshot overwrote it");
|
||||||
|
Assert.IsTrue(service.Settings.SingleClickActivates, "SingleClickActivates lost — a stale snapshot overwrote it");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user